A multi-tenant SaaS application serves multiple customer organizations through a shared product. Shared PostgreSQL tables can be a sensible starting point, provided every operation enforces the tenant boundary and the team tests that enforcement. Stronger separation becomes appropriate when customer contracts, operational requirements, or workload isolation demand it.
Define what a tenant represents
A tenant is usually the organization that owns the business data and subscription. It is not necessarily a user, a domain name, or a billing contact. Define it before designing access rules.
For a project management product, one person might belong to a consulting firm's workspace and several client workspaces. Their permissions can differ in each. Model users independently from tenants and connect them through memberships.
Identify shared records separately. A country list might be global. A project, invoice, integration credential, or uploaded contract normally belongs to a tenant. Ambiguity here becomes ambiguity in authorization, deletion, and billing later.
Write down ownership for every new data type. The question is not only who may see the record, but who can export it, retain it, delete it, or request restoration after a mistake.
Choose the isolation model deliberately
| Model | Useful when | Operational cost |
|---|---|---|
| Shared database and tables | Many tenants use the same product model | Strict filtering, constraints, and access testing |
| Separate schemas | Logical grouping supports an operational need | Repeated migrations and schema-aware tooling |
| Separate databases | Recovery or customer-specific boundaries justify separation | Provisioning, connection management, fleet migrations |
| Separate deployments | Dedicated infrastructure is a contractual requirement | More deployment, monitoring, and incident ownership |
These models describe boundaries, not automatic security guarantees. Separate schemas can still be accessible through an overprivileged connection. Separate databases on one server can still share a failure domain.
Our default for an ordinary SaaS MVP is shared tables with explicit tenant ownership and carefully scoped access. Avoid promising a future enterprise buyer physical isolation simply because the schema uses a tenant column. Contract language should match the deployed system.
Enforce relationships inside the tenant
Put a tenant identifier on tenant-owned records. Use it in the application's resource lookups and in constraints where identifiers must be unique within an organization.
For example, an invoice number might be unique within a tenant rather than across the entire product. A task's project reference should not allow the task's tenant to differ from the project's tenant. Composite keys and foreign keys can encode that invariant when the schema is designed for it. PostgreSQL's constraints documentation explains the underlying mechanisms.
Do not rely only on a friendly validation query before writing. Concurrent requests can change the situation between the check and the insert. Let the database reject invalid relationships, then convert that rejection into a useful application response.
Indexes should reflect tenant-scoped queries. A list that filters by tenant and sorts by creation time is a concrete access pattern to examine. Use representative data and query plans rather than assuming a tenant column alone makes queries efficient.
Treat row-level security as another boundary
PostgreSQL row-level security, or RLS, can restrict which records a database role may read or change. Once enabled, a table without an applicable policy denies normal row access. Table owners usually bypass RLS, while superusers and roles with BYPASSRLS bypass it. The official RLS guide documents these exceptions.
That makes the application connection role part of the architecture. Test the exact role used in production, not only the privileged migration account. Review reads and writes, including whether an update can move a record into another tenant.
If policies rely on request context supplied by your server, establish that context only after verifying the user's membership. With pooled connections, keep the context correctly scoped to the transaction so it cannot carry over to another request. A client-controlled tenant parameter must never become trusted authorization context without validation.
RLS does not authorize a file download from an unrelated storage path. It also does not protect work performed through a privileged connection. Map those boundaries explicitly rather than treating a policy as coverage for the whole application.
Carry tenant context outside the database
A cache key such as project:123 may be unsafe if project identifiers are only locally unique or the cached response includes user-specific permissions. Include the tenant and any relevant authorization scope, and invalidate or refresh cached results when those permissions change.
Store uploaded files under tenant-aware identifiers. Generate download access after checking current permission. A difficult-to-guess URL is not an access policy for a private contract.
Background jobs should contain an immutable tenant identifier, the requesting identity when relevant, and the resource being processed. The worker must apply the same business boundary as the API. A global administrator connection with an accidentally unfiltered export can bypass careful request-layer controls.
Include tenant context in integration credentials and webhook routing. A provider account identifier should resolve to a stored, authorized tenant mapping. Do not select the target organization solely from an unsigned field in an inbound payload. See reliable webhook design.
Plan for noisy neighbors and recovery
Isolation includes availability. One tenant generating a large export should not consume every worker slot or database connection. Apply per-tenant limits to expensive work and leave capacity for ordinary requests.
Measure queue age, query latency, resource usage, and failure rate with enough tenant context to identify an uneven workload. Keep observability access controlled; diagnostic records can expose customer information too.
Shared-table backups create a recovery question. Restoring the entire database may roll back every tenant when only one customer needs an old record. A targeted recovery may require restoring into a separate environment, extracting the relevant data, checking relationships, and importing carefully.
Define that process before promising tenant-level restoration. Export and deletion also need to cover files, external integrations, job payloads, and retained audit data. The database schema alone does not describe the full lifecycle.
Test with hostile combinations
Create tenants A and B, with owner, editor, and viewer identities. For each protected endpoint, substitute B's resource identifiers into A's authorized request. Repeat the test against reads, writes, searches, exports, downloads, and queued tasks.
Test membership removal during an active session. Test a background job that runs after its requester loses access. Check both database policy behavior and business-level rules, since either layer can fail independently.
Treat a cross-tenant result as a release blocker. A small product can defer customization and advanced reporting; it cannot defer the boundary that keeps customer records separate. The authentication guide and SaaS architecture guide provide the surrounding application design.