Multi-tenancy is an authorization problem before it is a database-layout decision. A shared table can be safe with rigorously enforced controls, while separate databases can still leak data through a cache key, object-store path, search index, model prompt, or support tool.
The design goal is simple to state: every read, write, retrieval, export, and operational action must stay inside the authorized tenant boundary. Achieving it requires carrying a trusted tenant context through the entire request path.
Establish one trusted tenant context
Every tenant-owned record needs an immutable tenant identifier. Do not accept that identifier as proof of access merely because it appears in a request body or URL. Derive the allowed tenant from the authenticated session, service identity, or verified integration installation, then compare it with the requested resource.
Include tenant context in database access, background jobs, message envelopes, object storage paths, vector records, cache keys, traces, and audit events. A queue consumer should not infer a tenant from an untrusted document field. Its job payload should carry a signed or server-created tenant reference and retrieve credentials scoped to that tenant.
Use opaque identifiers rather than customer names or email domains. Human-readable names change and can collide. If users can belong to more than one tenant, make the active tenant an explicit, authorized session choice.
Row-level security is a guardrail, not decoration
PostgreSQL row-level security (RLS) can enforce which rows a database role may see or change. Enable it on every tenant-owned table, define policies for all relevant operations, and use a default-deny posture. Pay special attention to table owners and privileged roles that may bypass policies.
Application filters such as WHERE tenant_id = ? remain useful for clarity and performance, but they should not be the only barrier. Central enforcement reduces the chance that one forgotten filter becomes a cross-customer incident.
Test RLS with the same database roles used in production. Tests that run as a table owner or superuser can pass while completely bypassing the policies they are supposed to validate. Include negative tests that attempt to read, update, join, and export another tenant's data.
Choose the right physical isolation model
There are three common patterns:
Shared tables with a tenant column offer the highest density and simplest migrations. They require mature RLS, tenant-aware indexes, careful noisy-neighbor controls, and disciplined query review.
Separate schemas create a clearer namespace and can simplify tenant-specific export or cleanup. They increase migration and connection-management complexity, and a wrongly selected schema can still cross a boundary.
Separate databases or accounts provide a stronger isolation boundary and may fit contractual, regional, performance, or customer-managed-key requirements. They cost more to provision, patch, observe, and migrate at scale.
These choices are not moral rankings. Use a documented decision based on data sensitivity, regulatory and contractual needs, customer size, blast radius, operational maturity, and cost. A tiered model can place most tenants in a pooled system while assigning dedicated environments where requirements justify them.
Partitioning is not the same as isolation
Database partitions, shards, and storage prefixes can improve performance and lifecycle management. They do not automatically provide authorization. AWS's SaaS guidance explicitly distinguishes data partitioning from tenant isolation. Keep the security control independent from the physical placement mechanism.
The same warning applies beyond the primary database. Vector indexes need tenant and permission metadata. Cache keys must include the tenant. File paths should be scoped by tenant and protected by server-side authorization rather than predictable URLs. Analytics exports should preserve tenant boundaries or use deliberately aggregated, de-identified datasets.
Retrieval systems need a finer document-level version of this control, covered in permission-aware RAG.
Common failure modes
- Trusting a client-supplied
tenant_idwithout checking membership. - Applying RLS to primary tables but not join tables, search indexes, or exports.
- Running the application with an owner or administrator role that bypasses policies.
- Omitting tenant context from queue jobs, cache keys, file paths, or vector metadata.
- Calling a database-per-tenant design “isolated” while sharing credentials or admin tooling broadly.
- Letting support impersonation occur without approval, expiration, and audit evidence.
- Measuring only functional success and never running adversarial cross-tenant tests.
Implementation checklist
- Inventory every place tenant-owned data is stored, copied, indexed, cached, or logged.
- Derive tenant context from authenticated server-side state.
- Require tenant identifiers on owned records and enforce relational constraints.
- Enable and test RLS with least-privilege production-equivalent roles.
- Document why pooled tables, schemas, or databases fit each customer tier.
- Include tenant context in asynchronous work and idempotency keys.
- Test cross-tenant reads, writes, joins, exports, search, and support access.
- Automate tenant deletion and export across downstream copies.
- Audit privileged access and review it regularly.
Measurable signals
Track the percentage of tenant-owned stores covered by an enforced policy, negative isolation tests passing in CI, privileged-role usage, cross-tenant authorization denials, records missing tenant IDs, tenant provisioning and deletion time, noisy-neighbor saturation, and audit-log coverage. Any unexpected cross-tenant access is a security incident, not an acceptable error-rate metric.
Further reading
Put this into practice
Review your tenant isolation design
Bring one real system or customer workflow and map the next practical decision.
Run the workflow readiness audit
Test the workflow, evidence, and control assumptions before committing to a build.
See the delivery path
See how Tier9AI scopes, controls, and delivers one production workflow.