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
slugand 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
featuresJSON 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:
- Reads the bearer token.
- Resolves the principal's
tenant_idfrom the ScaiKey JWT claim. - Loads the tenant row + its limits and features.
- Attaches a
TenantContext(frozen dataclass) torequest.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#
- Federation.
- Bridges.
- API: Admin.
- Reference: Configuration.