larsggu.me › Reference › tenant isolation
tenant isolation
every query, key and cache entry carries tenant_id
The guarantee that one customer's data in a shared system cannot be read, written or inferred by another, enforced at the query layer rather than in application logic.
Description
A system serving many organisations from one deployment has one dominant failure mode, which is a request from one organisation returning another's rows. Every other defect can be put right afterwards. This one cannot be, because a disclosure cannot be withdrawn.
The reliable constructions put the tenant boundary somewhere a developer cannot forget it. A separate database or schema per tenant makes the boundary physical. A shared table with row-level policies makes the boundary a property of the connection rather than of the query text. What does not work over time is a convention that every query must include a tenant clause, because the convention holds until the day somebody writes a report query in a hurry.
The boundary also has to cover the things that are not the database. Cache keys, object storage prefixes, search indexes, queue names, exported files, log lines and generated identifiers all carry tenant data, and each is a place where a missing prefix produces a cross-tenant read that no database policy will catch.
Identifiers themselves deserve attention. Sequential integers let one tenant infer how many records exist across the whole system and whether a neighbouring record exists at all. Opaque identifiers with enough entropy remove that inference and also remove the temptation to guess a neighbouring value.
One resolved identifier, applied at every store the request touches.
Fields
| Field | Form | Meaning |
|---|---|---|
| tenant_id | opaque identifier | Resolved from the credential, never read from the request body. |
| Enforcement point | connection or schema | Where the boundary is applied. Application code is the weakest option. |
| Cache key prefix | tenant-scoped | Prevents a shared cache from serving one tenant's entry to another. |
| Object prefix | tenant-scoped | Applies the same rule to file and export storage. |
| Noisy-neighbour limit | per tenant | Quota measured per tenant so one workload cannot consume the shared capacity. |
Example
Resolving the tenant from the credential
GET /v1/invoices/in_3390 HTTP/1.1
Authorization: Bearer at_...
# tenant_id is resolved from the token, not from any request parameter
# and is applied by the connection policy, not by the query text
SET LOCAL app.tenant_id = 'tn_44';
SELECT id, total, state FROM invoices WHERE id = 'in_3390';
HTTP/1.1 404 Not Found
{"error":{"type":"not_found","message":"No invoice with that identifier."}}A record belonging to another tenant is reported as absent rather than forbidden, so its existence is not disclosed.
Failure modes
- Accepting a tenant identifier from the request body or a header a caller can set.
- Enforcing the boundary in application code, where one forgotten clause is enough.
- Prefixing the database but not the cache, so a warm entry crosses the boundary.
- Using sequential identifiers, which disclose volume and adjacency across tenants.
Related entries
Topic: Operations. Last modified 2026-09-06.