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

curl https://relata.example.com/query \
  -H "Authorization: Bearer $RELATA_BEARER_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.

HeaderDescription
X-Organization-IdThe tenant the request runs as. Required on server/cluster.
X-Acting-AsUser identity the request runs as (delegated).
X-Delegated-ByThe principal who granted the delegation.
# User alice delegates to bob for an analytics query
curl https://relata.example.com/query \
  -H "Authorization: Bearer $RELATA_BEARER_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-acme

HTTP API

Three API surfaces serve different audiences:

SurfaceAudienceKey difference
/tenantsTenant adminFull CRUD + members + sharing + quota + search config
/api/v1/tenantsControl-plane automationHard-purge delete, billing-grade usage, inline quota at create
/platform/tenantsPlatform operatorTier assignment, license status, cross-tenant usage
# Create
curl -X POST http://localhost:9090/tenants \
  -H "Authorization: Bearer $RELATA_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"org-acme","name":"Acme Corp","tier":"standard"}'
 
# Set quota
curl -X PUT http://localhost:9090/tenants/org-acme/quota \
  -H "Authorization: Bearer $RELATA_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"max_mb":10240}'
 
# Check usage
curl http://localhost:9090/tenants/org-acme/usage \
  -H "Authorization: Bearer $RELATA_BEARER_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_BEARER_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_BEARER_TOKEN"

For control-plane automation (billing, hard purge):

# Provision with inline quota
curl -X POST http://localhost:9090/api/v1/tenants \
  -H "Authorization: Bearer $RELATA_BEARER_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_BEARER_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_BEARER_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.

VariableDefaultDescription
RELATA_QUERY_QUOTA10000Cost 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_BEARER_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:

# Ingest into org-acme
curl -X POST http://localhost:9090/ingest \
  -H "Authorization: Bearer $RELATA_BEARER_TOKEN" \
  -H "X-Organization-Id: org-acme" \
  -H "Content-Type: application/json" \
  -d '{"object_type":"Person","data":[{"name":"Ada"}]}'
 
# Query from org-other — should return zero rows
curl -X POST http://localhost:9090/query \
  -H "Authorization: Bearer $RELATA_BEARER_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.

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