Tenant isolation across services and storage

Tenant isolation is a property of the complete system. A correctly filtered database query is insufficient if a cache, background job, file URL, or support tool can cross the same boundary.

Include the tenant in cache keys

Two tenants have an invoice numbered 42. The cache must distinguish them after the active tenant has been verified.

If the tenant is omitted

Tenant northLocal ID 42Cache key invoice:42
Tenant southLocal ID 42Cache key invoice:42

Both requests select the same cache entry. A response cached for north could be returned to south if the cache layer omits the tenant boundary.

Use a separate key for each tenant

Tenant north
tenant:north:invoice:42
Tenant south
tenant:south:invoice:42

Use the tenant ID when reading, writing and invalidating cache entries. Check resource permissions against the verified tenant on every request.

Cache hits bypass database filters

A cache hit can return a response without querying the database. Even a correctly scoped query is never reached in that path. Review the cache read, write and invalidation boundaries alongside the database policy.

Cache identities must distinguish each tenant’s records. Verify membership and resource permissions independently of the cache key.

Establish trusted tenant context at entry

Derive the active tenant from an authenticated membership relationship and validate any requested tenant selection against it. A tenant identifier in a header or URL is a selector, not proof of membership. Bind the verified context to the request and ensure downstream services receive it through an authenticated mechanism.

Decide how users belonging to multiple tenants switch context. Avoid ambiguous defaults, and make the current context clear in interfaces that perform writes or exports. Support and administrative identities require separately reviewed privileges; an internal network location alone does not justify access to every tenant.

Choose a storage isolation model

Separate databases, separate schemas, and shared tables offer different isolation and operational tradeoffs. A shared table design needs reliable tenant scoping on every access path. Database row security can add protection, but its behavior depends on roles, ownership, policies, and connection management.

PostgreSQL superusers and roles with BYPASSRLS bypass row security. Table owners normally bypass it too, although FORCE ROW LEVEL SECURITY subjects owners to policies; it does not constrain superusers or BYPASSRLS roles. Use a restricted application role, scope tenant context to the transaction, and test connection pool reuse so one request cannot leave context active for another.

SurfaceIsolation requirementUseful test
DatabaseVerified tenant condition and restricted roleCross tenant read and write attempts
CacheTenant included in cache identitySame resource key in two tenants
Object storageScoped paths and access checksForeign object URL access
QueueVerified tenant context per messageReused worker across tenants

Isolate jobs, caches and exports

Carry tenant context through jobs, event handlers, search indexes, analytics projections, and generated exports. A background worker should validate its message contract and authorize the intended operation instead of trusting an arbitrary tenant field. Keep deduplication and idempotency scopes explicit when identifiers may repeat across tenants.

Include tenant identity in cache keys and invalidation operations. Apply access rules to relationship traversal, counts, and error messages as well as record bodies. Aggregated information can still reveal another tenant's activity. Review temporary files and signed download URLs because they often outlive the request that created them.

Test isolation and resource fairness separately

Build adversarial tests using identical local resource identifiers in two tenants. Exercise every route and job type with mismatched context, missing context, and a revoked membership. Include exports, retries, dead letter replay, and privileged support paths in the test inventory.

Data isolation and capacity isolation are related but distinct. A tenant can stay within its data boundary while exhausting a shared queue or database pool. Apply appropriate quotas, concurrency limits, and observability by tenant without creating unbounded metric cardinality. Review onboarding, offboarding, and deletion as lifecycle operations across all derived stores.

References