Failure modes

Enforcing tenant isolation beyond the database

Cross-tenant exposure in multi-tenant systems most often arises in caches, real-time channels, queues and object storage rather than in database queries. A structural approach closes each path.

By T P Joshi · · Updated · Architecture · 3 min read

Where cross-tenant exposure occurs

Most multi-tenant systems scope the database carefully: a tenant identifier on every table and a filter on every query. Exposure tends to appear where data moves without a query:

  • a cache whose keys omit the tenant, so that one tenant's entry is served to another;
  • a real-time room or channel that every tenant's administrators join;
  • a search index that is queried without a tenant filter;
  • a background job whose payload does not say which tenant it belongs to;
  • object-storage paths that are shared across tenants.

In each case a correct database query can still return or broadcast another tenant's data, because the boundary was enforced at the database and nowhere else. These layers also receive the least attention in review, since they seldom appear in the data model.

One rule for every shared medium

Isolation holds when every medium shared between tenants carries the tenant explicitly.

MediumRequirement
CacheThe tenant identifier is part of every key
Real-time channelsRoom names include the tenant, and joining is authorised against tenant membership on the server
Queues and jobsThe payload carries the tenant, and workers assert it before acting
SearchA tenant filter, or a separate index, applied on the server and never taken from client input
Object storageKeys are prefixed by tenant, and access is through short-lived signed URLs
Logs and metricsThe tenant is a dimension on every record, so that incidents can be scoped
ConfigurationPer-tenant settings live in data, not in per-tenant environment variables or code branches

Enforcing the rule structurally

A rule that depends on individual attention fails eventually, usually on a tired day. It has to be enforced by the system:

  • Resolve the tenant once, at the edge. Derive it on the server from the hostname or the authenticated session, never from a client-supplied field, and carry it in a request-scoped context that data-access code cannot bypass.
  • Make unscoped access an error. Keep an explicit list of tenant-scoped models and have the data layer refuse a query that omits the scope. Database row-level security adds a second layer beneath the application.
  • Gate changes in CI. The build fails when a new model is not classified as tenant-scoped or global, and a check compares the scope of each query with that list.
  • Test the negative case. For every shared medium, a test acts as a user of tenant A against the resources of tenant B and expects a refusal.
  • Separate the operator plane. Cross-tenant access for internal tooling belongs in a distinct deployment with its own authentication and audit trail, unreachable from the tenant-facing application.

Auditing and keeping the record

An isolation audit lists every shared medium, states what was checked and what was found, and is kept alongside the code. The record tells the next engineer what has been verified and what has not. A new shared medium, such as a new queue or cache, enters the audit as part of the definition of done.

The cost of doing it early

Adding the tenant dimension before the second tenant arrives is a modest design cost. Retrofitting it after an incident means auditing every shared layer under pressure, and may mean notifying affected customers.


Jversity was audited against this rule before external tenants were onboarded. The case study describes the platform.