---
audience: developer
summary: Every entity in ScaiWave is scoped to a tenant. What that means in practice
  for isolation, billing, and operations.
title: Multi-tenancy
path: concepts/multi-tenancy
status: published
---

# 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](/docs/scaiwave/concepts/federation).
- **Bridges** — asymmetric webhook protocol; a foreign system relays
  messages in and out via signed HTTP. See [Bridges](/docs/scaiwave/concepts/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](/docs/scaiwave/concepts/federation).
- [Bridges](/docs/scaiwave/concepts/bridges).
- API: [Admin](/docs/scaiwave/reference/api/admin).
- Reference: [Configuration](/docs/scaiwave/reference/configuration).
