Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Multi-tenancy

ScaiWave is multi-tenant at every layer. There is no "global" data — every row in every table carries a tenant_id, every API request resolves to a single tenant, every Redis key is namespaced, every queue job carries the tenant in its payload.

What a tenant is#

A tenant is an isolated organisation: a company, a team, a project. Tenants get:

  • A unique slug and a friendly name.
  • A scaikey_tenant_id — the same tenant, identified to the identity provider.
  • Optional partner_id — for partner-managed tenants under a parent organisation.
  • Limits (max participants, max rooms, max storage, AI tokens monthly).
  • A features JSON for per-tenant feature flags (prompt_studio.enabled, sidekick_limits.max_concurrent, etc.).
  • Configurable AI defaults (enabled models, default model, system prompt templates, ScaiGrid auth mode, ScaiGrid API key when in per-user mode).

Resolution#

Every request flows through middleware (TenantMiddleware) that:

  1. Reads the bearer token.
  2. Resolves the principal's tenant_id from the ScaiKey JWT claim.
  3. Loads the tenant row + its limits and features.
  4. Attaches a TenantContext (frozen dataclass) to request.state.

Every service constructor downstream takes that TenantContext. There is no global "current tenant" — everything is parameter-passed, so cross-tenant leakage requires a deliberate bug, not just forgetting.

Database scoping#

Every query that returns user data filters on tenant_id. SQLAlchemy sessions don't enforce this — the convention is enforced by code review and the service layer being the only path to mutations.

There is no row-level security in the database; tenants are not strongly isolated at the storage level. If you need that, you'd run separate ScaiWave instances per tenant, with their own DBs.

Redis scoping#

Every Redis key uses the pattern sw:{tenant_id}:{namespace}:{key}, e.g. sw:abc-123:presence:user-xyz, sw:abc-123:sidekick_token:task-456. Cross-tenant key collisions are impossible.

NATS scoping#

JetStream subjects are tenant-scoped via the room id (which is tenant-scoped). Consumers can subscribe to swp.room.*.> patterns without seeing cross-tenant events.

Partners#

A partner is an organisation that operates a fleet of tenants on behalf of clients. Partners get their own admin API surface (/v1/admin/partners/*) and partner-scoped views. A tenant's partner_id (nullable) records the parent.

This is meaningful for billing, support, and the "manage tenants on behalf of customers" workflow.

Quotas and limits#

Limit Default Where it's enforced
max_participants 100 At join time.
max_rooms 500 At create-room time.
max_storage_mb 10240 At upload time.
ai_tokens_monthly_limit null Accounting via ScaiGrid.
Sidekick concurrency 3/user, 10/tenant At spawn time.
Plan steps 20/plan At plan write time.

Most can be overridden per tenant by an admin.

When tenants talk to each other#

Two ways:

  • Federation — symmetric, signed protocol; a foreign participant joins your room as a real member. See Federation.
  • Bridges — asymmetric webhook protocol; a foreign system relays messages in and out via signed HTTP. See Bridges.

Neither breaks tenant isolation. Federated events are stored in your DB as tenant_id = <your-tenant>, with a foreign_origin flag.

Where to go next#

Updated 2026-05-17 13:10:02 View source (.md) rev 3