Multi-Tenancy
RelataDB supports multiple organisations on a single binary. Isolation is enforced on the read path via planner guards plus tenant_id keying in the storage layer. One tenant cannot see another's rows, even on a shared node.
How isolation works
Every scan carries the request's tenant context into storage. The planner guard rejects any query plan that would cross tenant boundaries. Rows physically co-exist in one store but are logically unreachable across tenants.
There is no default tenant. A request to server or cluster without X-Organization-Id is rejected.
Set the tenant header on every request
$TENANT_TOKEN below is a tenant-scoped token minted via POST /admin/tokens (see Auth & Security) — RELATA_BEARER_TOKEN itself never authenticates data-plane routes like /query, on any profile.
curl https://relata.example.com/query \
-H "Authorization: Bearer $TENANT_TOKEN" \
-H "X-Organization-Id: org-acme" \
-H "Content-Type: application/json" \
-d '{"sql":"SELECT name FROM Person LIMIT 5"}'The SDK injects the header automatically when you set tenant:
# Python SDK
from relata import RelataClient
client = RelataClient(
url="http://localhost:9090",
bearer_token=token,
tenant="org-acme",
purpose="analytics",
)
rows = client.query("SELECT name FROM Person LIMIT 5")Delegation headers
Use delegation when one principal acts on behalf of another. Every delegation is recorded in the audit log and validated against the delegating principal's authority.
| Header | Description |
|---|---|
X-Organization-Id | The tenant the request runs as. Required on server/cluster. |
X-Acting-As | User identity the request runs as (delegated). |
X-Delegated-By | The principal who granted the delegation. |
# User alice delegates to bob for an analytics query
curl https://relata.example.com/query \
-H "Authorization: Bearer $TENANT_TOKEN" \
-H "X-Organization-Id: org-acme" \
-H "X-Acting-As: user:bob" \
-H "X-Delegated-By: user:alice" \
-H "Content-Type: application/json" \
-d '{"sql":"PURPOSE '\''analytics'\'' SELECT name FROM Person LIMIT 5"}'A delegation that exceeds the delegating principal's authority is rejected by the policy engine. The audit entry records both principals.
Create and manage tenants
Tenant management uses the /tenants REST API. Start the server with RELATA_TENANCY_MODE=multi and an admin bearer token.
CLI
The relata tenant subcommand wraps the full lifecycle:
# Create
relata tenant create --id org-acme --name "Acme Corp" --tier standard
# List / get / update
relata tenant list
relata tenant get org-acme
relata tenant update org-acme --name "Acme Global"
# Quota
relata tenant quota org-acme --max-mb 10240
# Usage
relata tenant usage org-acme
# Members
relata tenant members org-acme --add user-1 --role analyst
# Sharing agreements
relata tenant sharing org-acme --add partner-org
# Suspend / reactivate / delete
relata tenant suspend org-acme
relata tenant reactivate org-acme
relata tenant delete org-acmeHTTP API
Three API surfaces serve different audiences:
| Surface | Audience | Key difference |
|---|---|---|
/tenants | Tenant admin | Full CRUD + members + sharing + quota + search config |
/api/v1/tenants | Control-plane automation | Hard-purge delete, billing-grade usage, inline quota at create |
/platform/tenants | Platform operator | Tier assignment, license status, cross-tenant usage |
/tenants/* is dual-gated: a sys-admin credential (RELATA_ADMIN_TOKEN) manages any tenant; a tenant-scoped registry token ($TENANT_TOKEN) is narrowed to that caller's own tenant. RELATA_BEARER_TOKEN is valid for neither — it is never a data-plane or control-plane credential.
# Create (sys-admin only — the tenant doesn't have a token yet)
curl -X POST http://localhost:9090/tenants \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id":"org-acme","name":"Acme Corp","tier":"standard"}'
# NOTE: tenant creation is not idempotent once at the max_tenants cap — the
# license check runs before the existing-id check, so re-POSTing an id that
# already exists returns 403 {"code":"license-limit"} rather than a 2xx/409,
# even though the tenant is already there. A create-if-absent bootstrap
# script should GET /tenants (or /tenants/:id) first, or treat a
# license-limit 403 as "may already exist" and fall back to a GET.
# Set quota
curl -X PUT http://localhost:9090/tenants/org-acme/quota \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"max_mb":10240}'
# Check usage (a tenant's own $TENANT_TOKEN also works here, narrowed to itself)
curl http://localhost:9090/tenants/org-acme/usage \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN"
# Create a sharing agreement (org-acme shares with org-partner)
curl -X POST http://localhost:9090/tenants/org-acme/sharing \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"partner_org":"org-partner"}'
# Retire a tenant (governed tombstone)
curl -X DELETE http://localhost:9090/tenants/org-acme \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN"For control-plane automation (billing, hard purge) — sys-admin only:
# Provision with inline quota
curl -X POST http://localhost:9090/api/v1/tenants \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tenant_id":"org-acme","display_name":"Acme Corp","tier":"server","quota_mb":10240}'
# Hard purge (removes all tenant rows — irreversible)
curl -X DELETE http://localhost:9090/api/v1/tenants/org-acme \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN"See the HTTP API reference for the complete endpoint table covering all three API surfaces.
Per-tenant encryption
Each tenant has a distinct Data Root Key (DRK). KMS isolation means one tenant's DEK cannot unwrap another's data. The node fails closed if KMS is unavailable — no tenant data is readable in plaintext at rest.
DRK rotation produces a new wrapped-DEK set without re-encrypting data rows. To rotate:
curl -X POST http://localhost:9090/tenants/org-acme/rotate-key \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN"Per-tenant quotas
Quotas protect against noisy-neighbour workloads. Cost is a function of rows scanned, not rows returned — a SELECT * against a 10 M-row table costs more budget than a LIMIT 10.
| Variable | Default | Description |
|---|---|---|
RELATA_QUERY_QUOTA | 10000 | Cost units per principal per window. |
Set per-tenant overrides via the admin API (see above). Quota exhaustion returns 429 Too Many Requests with a Retry-After hint.
Monitor quota usage across all tenants:
curl http://localhost:9090/tenants/usage \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN"Multi-org principals
A principal may belong to multiple organisations. The X-Organization-Id header selects which org's data is in scope for each request. The acting org is recorded in the audit log alongside the principal identity.
Verify that cross-tenant isolation holds. Mint one token per tenant (POST /admin/tokens with a distinct tenant_id, see Auth & Security) — a tenant-scoped token is bound to the tenant it was minted for, so it cannot be pointed at another tenant just by changing X-Organization-Id:
# Ingest into org-acme, using org-acme's own token
curl -X POST http://localhost:9090/ingest \
-H "Authorization: Bearer $ACME_TOKEN" \
-H "X-Organization-Id: org-acme" \
-H "Content-Type: application/json" \
-d '{"object_type":"Person","data":[{"name":"Ada"}]}'
# Query as org-other, using org-other's own token — should return zero rows
curl -X POST http://localhost:9090/query \
-H "Authorization: Bearer $OTHER_TOKEN" \
-H "X-Organization-Id: org-other" \
-H "Content-Type: application/json" \
-d '{"sql":"SELECT * FROM Person"}'Zero rows from org-other confirms isolation is working. (A single, standing credential that could read any tenant just by switching X-Organization-Id used to be possible via RELATA_BEARER_TOKEN — that gap is closed: the env bearer token is never accepted on data-plane routes now, precisely because it had no tenant binding.)
Sub-tenant namespaces
NamespacePath on Row partitions data within a tenant — for example, separating departments or cases inside one organisation.
SELECT * FROM Person WHERE namespace = 'org-acme/dept-finance' LIMIT 5;Sub-tenant namespace enforcement is partially wired. Full per-namespace policy enforcement is deferred. Do not rely on namespace filtering as a security boundary today; use tenant-level isolation for hard boundaries.
See also
- Auth & Security — ABAC policy engine, egress filtering
- Configuration — quota and auth env vars
- Observability — audit trail per tenant