The tenant flow¶
How a tenant comes into existence, how its data is placed, and how the binding reaches the database.
Provisioning (the operator's side)¶
python -m examples.enterprise.app.bootstrap --slug acme --name "Acme Inc"
└── TenantRegistry.create(...) the tenant's row in the control plane
└── TenantRegistry.provision(...) the tenant's migration, tables and records
└── (the documented operator step) install_tenant_rls(...) -- the database's own enforcement
└── (the serving role's grants) grant_control_privileges(...) -- what the request path reads
bootstrap.py is idempotent: running it twice does not create a second tenant, and the operator
steps it performs are the ones the README documents. dataplane.py holds them so the tests can run
the same code the operator runs.
Placement¶
A tenant's data lives according to its PlacementStrategy. The example uses SHARED (one schema,
one set of tables, the tenant as a column) because that is the strategy whose failure mode — a
forgotten filter — row-level security exists to close. The strategy is data, not code: the plane is
built with the strategies it supports, and a tenant's row says which one applies.
The binding, end to end¶
TenantContext (jdlib.context)
├── tenant_id, tenant_slug, strategy
├── principal (kind, id, scopes)
└── request_id, correlation_id
│
├─ context_scope(...) bound for the request; `current_tenant()` reads it
├─ the plane's session the strategy sets `app.tenant_id` on the connection
├─ row-level security the database filters on that setting
└─ the audit writer records the tenant on every row
Three consequences worth stating plainly:
- The tenant is not a parameter. No route, service or repository in the example takes one; a
header or query parameter claiming a tenant is ignored, and
tests/security/test_tenant_isolation.pyasserts that against the answer. - An unbound session reads nothing. With RLS installed, a session that never set the tenant sees no rows — the failure mode is empty, not everything.
- The graph is bound the same way.
GraphClientcarries the tenant, and a statement that would cross tenants raisesGraphTenantViolationErrorrather than returning another tenant's nodes.
Cross-tenant answers, and why they are not 403¶
A read for a row that is not this tenant's is answered 409 INVALID_REFERENCE ("does not exist in
this tenant") — the library's own mapping, which the example deliberately does not re-map. A 403
would confirm that the id exists somewhere; the answer given confirms nothing.