Skip to content
Aldridge Dagos Get in touch

N°009 · 2026.05.15 · 6 MIN · Updated 2026.07.24

Multi-Tenant Data Isolation Belongs in the Database

By Aldridge Dagos, operations software engineer


A steel wall dividing two dark spaces, a seam of warm light along the edge.
Isolation holds when the database enforces it. Code can forget a filter. The rule does not.

When one product serves many companies, multi-tenant data isolation is the rule that keeps each company inside its own records. One running system can hold dozens of businesses, each with contacts, notes, history, deals, and documents. None of those rows can cross the company boundary, even when a new screen, report, or query gets added later.

I use this design in TractData, where land teams share one product while every workspace keeps its own parcels, owners, offers, outreach, and audit history.

The short version: Multi-tenant data isolation belongs in the database because application code protects one query at a time. One forgotten workspace filter can return rows that belong to another customer. PostgreSQL row security policies apply access rules when rows are selected, inserted, updated, or deleted. When row-level security is enabled and no policy grants access, PostgreSQL defaults to denying the row. The application can still add its own filters, but the database remains the final boundary under every path.

There are two places to enforce that boundary. You can repeat it in application code wherever data is requested, or you can attach the rule to the table that owns the rows.

Where the boundary lives What protects each query What happens when a new query forgets the tenant filter
Application code A filter written into that query The query can return another tenant’s rows
Database row-level security A policy attached to the table The database filters or rejects the row before it leaves

The second design has a smaller trust surface. The team still writes careful queries. The product simply does not depend on every future query being perfect.

Why is application code the weaker tenant boundary?

Application code sees each request separately. A list page adds a workspace filter. A report adds another. A background job adds a third. An export, search route, admin tool, or new dashboard needs the same rule again. The boundary now exists as a convention repeated across the codebase.

That convention works until one path omits it. The query still runs. The page can still look normal. The mistake is silent because the application asked the database for too many rows and the database had no rule that said no.

Tests help, but the unknown query remains the hard part. You can test the routes that exist today. You cannot test the route someone will write six months from now. A tenant boundary that depends on perfect repetition becomes harder to trust as the product grows.

The database has a better vantage point. It sees every row and every operation. That makes it the right place for the rule that must hold under all of them.

How does row-level security isolate tenant data?

Every customer-owned table carries a workspace or tenant identifier. The database compares that identifier with the workspaces the signed-in person may access. If the row belongs to an allowed workspace, the operation can continue. If it does not, the row stays unavailable.

PostgreSQL applies row security policies to normal row access for SELECT, INSERT, UPDATE, and DELETE. A practical row-level security guide describes the policy as an implicit WHERE clause added to every query. For new rows, a WITH CHECK policy verifies that the row being written also belongs inside the allowed boundary.

That distinction matters. Read isolation stops one company from seeing another company’s records. Write isolation stops a request from inserting or moving a record into a workspace it does not control. A complete boundary covers both directions.

The application should still pass the workspace identifier in its query. That keeps requests focused and makes intent obvious. I treat it as a performance and clarity filter, not the final defense. If that filter disappears, the database policy still decides which rows may leave.

What does a strong row-level security policy include?

A useful policy system answers a few exact questions:

  • Who is asking? The database receives an authenticated identity, not a workspace name supplied by the browser.
  • Which workspaces can that person access? Membership lives in a trusted table the person cannot edit into a higher role.
  • Which operations can that role perform? Reading, creating, changing, and deleting can use different rules.
  • What should happen with no matching policy? Access should fail closed, with no row returned and no write accepted.
  • Can privileged paths bypass the rule? Administrative functions and service credentials need a narrow purpose, a separate audit trail, and tests against the bypass.

The policy should stay small enough to inspect. One membership function can answer which workspaces belong to the signed-in person, then each customer-owned table can reuse that answer. The shape stays consistent across contacts, parcels, offers, notes, calls, and every table added later.

This is the same design principle behind preventing double-booking with a database constraint. If a rule must survive every code path, attach it to the data instead of asking each screen to remember it.

How do you verify multi-tenant data isolation?

Source review is only the first pass. The real proof comes from querying the running system as different identities.

I create two test tenants with distinct records. I sign in as the first tenant and try every read and write path against the second tenant’s identifiers. I test direct table reads, updates, deletes, exports, storage paths, and any database function that accepts a workspace identifier. Then I reverse the test. The expected result is simple: the allowed tenant works, the other tenant returns nothing or rejects the operation.

I also inspect the database itself. Every exposed customer-owned table should have row-level security enabled. Every operation the product needs should have a matching policy. A table that denies everything can look like an empty application, so verification must confirm both halves: the wrong tenant is blocked and the right tenant still works.

Code-only scans cannot prove this boundary because the policy may live below the code they inspect. The audit has to include the database policies and a live cross-tenant test. That is how you distinguish an application with a real tenant wall from one with a naming convention.

Where should multi-tenant authorization live?

Put the final authorization decision beside the rows. Keep application filters for focus, speed, and readable code. Keep database policies for the boundary that cannot be skipped.

The same least-privilege rule also protects systems that read untrusted text. Give each part only the records and actions it needs. A component that can be fooled should never hold access to every customer by default.

Multi-tenant software earns trust one denied row at a time. Put the boundary where every path has to cross it.

Frequently asked questions

What is multi-tenant data isolation?

Multi-tenant data isolation keeps each customer’s records separate when one product and database serve many customers. A signed-in person may read and change rows for the workspaces they belong to, while rows owned by every other workspace stay unavailable.

What is row-level security?

Row-level security is a database rule that decides which rows a user or role may select, insert, update, or delete. The rule runs inside the database, so it applies even when the application query forgets its own tenant filter.

Should tenant isolation live in application code or the database?

The final boundary should live in the database. Application filters are still useful for focused queries and clear code, but a row-level policy protects the table under every query, including paths added later.

How do you test multi-tenant data isolation?

Create two tenants with different records, sign in as each identity, and test every read and write path against the other tenant’s identifiers. Review the database policies as well as the application code. The correct tenant must work and the wrong tenant must return nothing or reject the operation.