Multi-Tenancy
How CortexDB isolates tenants — the tenant = token audience model, the cloud_shared_saas preset, per-tenant scopes, and how to provision multiple tenants.
CortexDB has two layers of isolation, and understanding the split is the key to a multi-tenant setup:
- Tenant — a hard boundary carried by the token's
audclaim (e.g.cortexdb:tenant:acme→tenant_id: "acme"). A request can never see or address another tenant's data. - Scopes — the hierarchical addressing inside a tenant (
org:acme/dept:eng/user:alice). This is how you separate users, teams, workspaces, and agents within one customer.
There is no tenant_id field in the request envelope — the tenant is derived entirely from the
authenticated token. You isolate tenants by issuing each one tokens with a distinct aud.
1. Choose a deployment posture
The deployment preset sets the ceiling for every tenant:
| Preset | Use for |
|---|---|
cloud_shared_saas | Many isolated tenants sharing one deployment — the multi-tenant posture. Works self-hosted / on-prem too. |
on_prem_enterprise | A single enterprise that owns the whole on-prem deployment — permissive (allow: ["*"], nothing denied). |
cloud_private | A single tenant on dedicated cloud (global scopes become "their private pool"). |
dev_local | Local development only (unsigned actors allowed). |
Set it with CORTEX_DEPLOYMENT_PRESET=cloud_shared_saas.
On-prem / self-hosted multi-tenancy: use cloud_shared_saas, NOT on_prem_enterprise
Multi-tenancy runs fully on your own hardware — the cloud_ prefix names the security posture
(shared, tenants isolated), not the hosting. For multiple isolated tenants on-prem, set
CORTEX_DEPLOYMENT_PRESET=cloud_shared_saas. Do not use on_prem_enterprise for that: it is
allow: ["*"], deny: [] (verified live) — it does not deny cross-tenant reads/creates, so it
provides no tenant isolation. on_prem_enterprise is for a single organization that owns the
entire deployment.
What cloud_shared_saas allows and denies
The preset is the outermost tier of the four-tier stack (deployment → tenant → scope → actor) — a capability is allowed only if every tier allows it, and no inner tier can grant what the deployment denies.
Allowed (per tenant): create org / user / agent / ws scopes; scope.write; local /
holistic / descend reads; understanding.*; forget.* including forget.gdpr; import.* / export.*;
audit.read; blob.*; and policy.administer.{tenant,scope,actor} for tenant admins.
The isolation guarantees — what cloud_shared_saas DENIES
cloud_shared_saas denies, at the deployment tier (so no token can override):
scope.create.cross_tenantandscope.read.cross_tenant— no scope may cross a tenant boundary.scope.create.global— the cross-tenantglobal:*pool is unavailable.understanding.read.cross_scopeandaudit.read.cross_actor.
A denied request returns 403 POLICY_DENIED naming the capability. Isolation is reinforced
cryptographically: the audit log hashes each row with a per-tenant pepper
(body_hash = sha256(body ‖ tenant_pepper)), and API keys are scoped per-caller, so one tenant can
never probe another's.
Defaults on this preset: actor.require_signed: true, scope.auto_register: true,
audit.retention: P2Y, rate_limit.write: 100/s, rate_limit.read: 500/s.
2. Give each tenant its own token audience
This is the core of the setup. Every token carries an aud that names the tenant; validation rejects a
token whose aud doesn't match the deployment's tenant binding. The token claims:
| Claim | Meaning |
|---|---|
iss | The issuer — must be in the deployment's allowlist. |
sub | The actor, in type:id form (must equal the X-Cortex-Actor header). |
aud | The tenant binding, e.g. cortexdb:tenant:acme. |
exp | Expiry, max 24h from issue. |
caps / scopes | Optional narrowing — a token can restrict capabilities (scopes like scope.write:org:acme/*) but never expand them. |
There are two ways to mint per-tenant tokens:
Option A — Your own IdP (recommended for production)
Register your IdP's issuer and its public key (Ed25519 / RS256 / ES256) in the deployment's issuer
allowlist. Your IdP then mints a token per session with the right aud (the customer's tenant), sub
(the end-user or agent), a ≤24h exp, and any caps/scopes narrowing you want. CortexDB verifies the
signature and enforces the tenant boundary — you own the tenant→user mapping.
Option B — The in-binary minter
POST /v1/auth/tokens mints tokens, but the minter is not configured by default — it returns
503 NOT_CONFIGURED ("token minting is not configured on this deployment") until you enable it with
CORTEX_V1_MINTER_ENABLE=1. It also requires the auth.mint capability:
- Self-hosted: the
cloud_shared_saasandon_prem_enterprisepresets grantauth.mintat the deployment tier (verified live), so once the minter is enabled you can issue child tokens. - Managed cloud:
auth.mintis gated by plan tier (Starter+, scoped to your tenant); an operator token for provisioning multiple tenants comes from CortexDB (contact the team).
Provisioning many tenants needs operator/IdP-level control of aud
Within a single tenant, auth.mint lets you issue child tokens for your users. But minting tokens for
different auds — i.e. standing up new tenants — is an operator/IdP concern: run an IdP whose token
service sets aud per customer (Option A), or use an operator token on the managed cloud.
3. Lay out scopes inside each tenant
Everything for tenant acme lives under an org:acme/… tree. Scope types carry semantics:
| Type | Semantics |
|---|---|
org | Top-level tenant |
dept / team | Business unit / cross-functional unit |
ws | Workspace (collaborative) |
app | Application instance |
user / agent / service | End-user / agent / service-account owner |
A typical shape:
org:acme/dept:eng/user:alice # an end-user's memory
org:acme/dept:eng/team:platform # a shared team scope
org:acme/ws:project-x # a collaborative workspace
org:acme/agent:support-bot # an agent's memoryscope.auto_register: true means writing to a new path provisions it — no pre-creation step. Reads use
view: local (this scope only), holistic (this scope + ancestors), or descend (this scope +
children), all bounded by the tenant. See Scopes.
4. Tenant administration
cloud_shared_saas grants policy.administer.{tenant,scope,actor}, so a tenant admin manages who can
access which scope via scope-registry membership — PUT /v1/scopes/members?path=org:acme/dept:eng
replaces the owner/writer/reader list for that subtree. The creating actor is auto-added as an owner,
and delete is owner-gated. See Scopes API.
5. Verify a tenant's boundary
# Confirms the tenant the token resolved to
curl .../v1/auth/whoami -H "Authorization: Bearer <token>" -H "X-Cortex-Actor: user:alice"
# → { "caller": "user:alice", "tenant_id": "acme", "deployment_preset": "cloud_shared_saas", ... }
# The effective capability set for an actor+scope
curl ".../v1/policy/effective?actor=user:alice&scope=org:acme/user:alice" -H ...