KynetraDB tenancy model
Multi-tenancy
Tenant isolation here is structural rather than advisory: it lives in the key space and in the credential type, not in a middleware everyone has to remember to call. This page also states plainly what tenant erasure does not do, because an erasure endpoint that implies more than it delivers is a compliance liability.
Bulkhead · fence 1 of 2
Kind-prefixed isolation
Like a ship's watertight compartments, a breach in one tenant cannot flood another. Every tenant entity is kind-prefixed, so a tenant's reads, writes and exports address only its own compartment — the prefix is part of the index key, not a filter applied afterwards.
t_{project}___{kind}
t_acme___posts # tenant "acme", collection "posts"
t_acme___user # tenant "acme", auth users
t_globex___posts # a different compartment entirely
# Cross-tenant safety is structural, not checked:
# because the cascade scans a kind PREFIX, a project id containing an
# underscore could match a neighbour's namespace — so project ids are
# REJECTED unless '_'-free on every path, including the orphan sweep
# where no row exists to cross-check against.
This is what makes an unscoped credential see nothing. On the live demo deploy, the global
service key returns {"collections":[]} — correct behaviour, because
tenant data lives under t_<project>___coll:* and an unscoped credential
must not see it.
Bulkhead · fence 2 of 2
The platform-vs-project auth plane fence
Every credential carries a platform_identity bool, set once at resolution.
The checks consult it before any role or uid comparison, so a project-plane
credential can never assert platform authority — even while holding its own project's
jwt_secret.
| Credential | Plane | platform_identity | Can manage another org? |
|---|---|---|---|
| Global service key | platform | true | yes |
| Unscoped platform JWT | platform | true | yes |
| Studio session, genuinely super_admin | platform | true | yes |
| Per-project service key | project | false | no |
| Per-project anon key | project | false | no |
| Project end-user JWT | project | false | no |
is_super_admin, require_platform_admin and
can_manage_org all check the flag first. AuthCtx derives only
Debug + Clone — no Deserialize, no Default, no
struct-update sites — so the flag cannot be set from untrusted input or inherited from a
default. The design landed after three adversarial review rounds.
ProjectStatus
Lifecycle and limits
Status is resolved fail-closed: an unresolvable or unknown project is denied rather than allowed through. Note that Paused is deliberately not a hard stop — reads and token refresh keep working, so a paused tenant's users are not logged out.
| Status | Reads | Writes | Token refresh | Intent |
|---|---|---|---|---|
| Active | yes | yes | yes | Normal operation. |
| Paused | yes | no | yes | Reversible. Sessions survive; the tenant is not logged out. |
| Suspended | no | no | no | Enforcement action. |
Per-project quotas
Entity quotas are enforced per project, so one tenant cannot exhaust shared capacity.
Per-project QPS limits
Request rate is capped per project alongside the entity quota.
Signature-verified namespace
Namespace selection is signature-verified, so a client cannot simply assert a namespace.
Scoped reads cost O(tenant)
A per-kind index makes a tenant-scoped read cost O(that tenant). Ram is default-on; Disk is default-off. See benchmarks.
Ferry
Per-tenant export with auth-material redaction
A tenant's namespace exports as integrity-hashed NDJSON, read from one pinned
Engine::snapshot() — consistent with no writer quiesce and no torn output.
The insight worth stating: "this is my tenant's data" and "this is safe to
hand out" are different questions.
| Kind | In export? | Why |
|---|---|---|
auth_otp | excluded | Unsalted SHA-256 of a 6-digit code — brute-forceable in milliseconds. |
auth_refresh | excluded | Session token hashes; an export containing them is a session-replay vector. |
user.password_hash | emptied | The user row travels; the credential does not. |
Everything else under t_{pid}___ | included | Prefix-scoped, so no other tenant's or platform data can enter. |
Integrity is a canonical content hash over the header — which carries the project id — plus
every entity line, with attribute keys recursively sorted. A relabelled or tampered file
therefore fails verification before a single entity is written, and the importer
additionally takes --expect-pid. Egress is audited in the hash-chained admin
log, refused for Deprovisioning and Purged tenants, and byte-capped.
Ebb
Tenant erasure — and what it is not
Tenant decommissioning that states exactly what it deletes and no more. A purge tombstones
every t_{pid}___* entity, destroys the tenant's keys, then compacts.
It is idempotent and resumable by construction.
What a purge does
- Tombstones every entity under the tenant's kind prefix.
- Destroys the tenant's keys, so remaining bytes are unauthenticatable.
- Compacts, so the data is absent from the compacted checkpoint.
- Re-lists kinds after each sweep until none remain, then lets an honest live recount decide completion — not the loop's own counter.
- Leaves the project row and skips compaction if incomplete, so the purge can be re-driven rather than silently half-finishing.
Declared scope
"live checkpoint + retained WAL" The data becomes unreadable through every query path, absent from the compacted checkpoint, and unauthenticatable. That is the whole claim.
Related: the named guarantees for Bulkhead, Ferry, Ebb and Meridian; reliability for the cross-tenant takeover path and export account-takeover vector that review caught.