HTTP APIs

larsggu.meReference › 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.

Diagram showing a request carrying a token, the tenant identifier resolved from that token, and the same identifier applied to the database policy, the cache key prefix and the object storage prefix before any data is read.

One resolved identifier, applied at every store the request touches.

Fields

Fields of tenant isolation
FieldFormMeaning
tenant_idopaque identifierResolved from the credential, never read from the request body.
Enforcement pointconnection or schemaWhere the boundary is applied. Application code is the weakest option.
Cache key prefixtenant-scopedPrevents a shared cache from serving one tenant's entry to another.
Object prefixtenant-scopedApplies the same rule to file and export storage.
Noisy-neighbour limitper tenantQuota 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.

Topic: Operations. Last modified 2026-09-06.