A tenant ID selects; it does not grant permission
A tenant here is an organisational unit whose data and permissions should be separated, such as a customer company. A person may belong to multiple tenants. They must then be able to select one; that selection alone does not prove membership.
Treat tenant_id in a path, header or request body as a selection. The server checks whether the verified identity may act in the selected tenant and which role currently applies there. Even a signed token claim needs an appropriate trust contract: is the issuer correct, is the token valid for this API, and how current is the membership it contains guaranteed to be? OWASP Multi Tenant Security, Tenant Identification.
After this check, a clear internal context should pass through processing. Downstream components must not overwrite it with an unverified header. If context is missing for a tenant-bound operation, the application must reject it; a query without a tenant condition would not be a safe substitute.
Identify owned, shared and administrative data
Not every table must belong to a single tenant. A public product catalogue may deliberately be shared. Platform administration may explicitly handle multiple tenants. Document the classification so that ‘no tenant field’ does not accidentally mean ‘readable by everyone’.
On narrow screens, scroll the table sideways.
| Data class | Example | Required decision |
|---|---|---|
| Assigned to one tenant | Report, invoice, internal comment | Active context and appropriate permissions in the assigned tenant |
| Deliberately shared | Public templates | Which content and actions are actually public |
| Explicitly shared across organisations | Shared project space | A dedicated sharing rule with scope and revocation |
| Platform administration | Narrowly scoped support task | A separate privileged path and limited authority |
Relationships must also match. Giving a new report the correct tenant_id is insufficient if its attached file belongs to another organisation. Check referenced objects against the same business boundary. Database constraints can additionally enforce these invariants, but do not replace user permissions. The BOLA/IDOR foundations explain individual object access.
Choose a storage boundary you can actually operate
Separate databases, separate schemas and shared tables have different operational consequences. A separate database can provide an additional boundary if connections, credentials and administrative paths are separated accordingly. A shared privileged account for every database can negate part of that advantage. Shared tables require consistent tenant-specific queries and potentially additional database policies.
A common addition is Row-Level Security (RLS): the database restricts visible or modifiable rows through a policy. In PostgreSQL, the role actually used matters. Superusers and roles with BYPASSRLS bypass row policies; table owners normally do too, unless included through FORCE ROW LEVEL SECURITY. However, FORCE binds neither superusers nor BYPASSRLS roles. PostgreSQL 18: Row Security Policies.
This is not a recipe for enabling RLS in production without testing. Before rollout, examine tables, policies, write cases, privileged jobs, error paths and the actual connection role together. An accidentally unrestricted role makes correct policy text ineffective.
Reused connections must not retain a tenant
Connection pools share database connections between requests. If tenant A sets a session value and the connection later goes to tenant B, a leftover value may affect the next access.
In PostgreSQL, transaction-local context can be part of the solution: SET LOCAL applies only until the current transaction ends. It must be set in the same transaction as the queries depending on it. Ordinary SET generally has a different, session-based lifecycle. Outside a transaction block, SET LOCAL does not have the intended effect. PostgreSQL 18: SET.
The application must still begin every transaction with correctly verified context and commit or roll it back before returning the connection to the pool. Check the actual pool mode and secondary paths. An isolated database test using another role or no connection reuse does not prove this flow.
Preserve the boundary in caches, files and jobs
On narrow screens, scroll the table sideways.
| Handover | Concrete question | Suitable measure in your design |
|---|---|---|
| Response or application cache | Can two tenants hit the same cache entry? | Include the tenant and every other permission-dependent variant in cache identity, or do not share-cache these responses |
| Generated file | Who may use the file key or download link? | Assign the file to a verified tenant and authorise access appropriately again |
| Queue | Can the job run later without request context? | Pass the server-verified tenant and requester with binding; the worker checks the intended execution context |
| Permission change before job start | What happens after departure or revocation? | Explicitly decide whether and when the job must be reauthorised or cancelled |
| Logs and analysis | Can support staff or customers see another tenant's events? | Restrict access to stored events by tenant too |
A cache entry keyed only as report:17 would be problematic if this identifier occurs in each tenant. tenant:<geprüfte-id>:report:17 separates that case but is not a universal solution: different field permissions, languages or versions may require further variants. A cache hit must not bypass the permission decision either. Reading HTTP findings provides the HTTP foundations; here, internal application caches are also involved.
A worker often has more technical privileges than a normal user. A job must therefore not process arbitrary tenants solely because of those privileges. Define whether it executes a job already bindingly accepted or requires current user permissions. Both models need clear, traceable limits.
Test the transitions your application actually uses
Set up two synthetic tenants with deliberately similar records. Identical local object identifiers help expose incorrect cache and file associations. Use the actual application role and intended pool path in an isolated environment.
Test permitted access, the same access in the other tenant, a context switch on a reused connection and a background job after permission withdrawal in sequence. Compare result content, stored changes, cache hits and generated files. Also verify explicitly permitted sharing so the test does not simply block all use.
A new table or export path must enter this mapping. Where possible, derive affected data classes from the schema or an authoritative inventory. A manually maintained test list that forgets the same new path as the product code would create false confidence.
Rollout and rollback without opening the data boundary
Test tenant-isolation changes first with representative synthetic data and the actual roles. Data migrations, RLS and cache changes need a separate rollout plan. Versioning the cache namespace can deliberately exclude old ambiguous entries from the new flow; actual deletion and retention remain operational decisions.
If the new boundary blocks valid flows, close the affected function or return to a previously tested restrictive state. Disabling RLS or removing tenant filters without ensuring an equivalent boundary would not be safe rollback. The public API Security Check cannot test this separation in the database, caches or jobs.
For implementation, roles and permissions, field permissions and testing your own API security connect the individual decisions. Logging and monitoring covers observation and response.
Sources and date
Sources checked: 27 September 2026. Links also appear alongside the statements they support.