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.

2 isolation fences 3 ProjectStatus states t_{pid}___ kind prefix tombstone erasure, not crypto

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.

scoped_kind — the tenant namespace text
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

erasure_scope text
"live checkpoint + retained WAL"

The data becomes unreadable through every query path, absent from the compacted checkpoint, and unauthenticatable. That is the whole claim.