Skip to content

Work / Trunk / Wiki / Concepts

tenant-isolation

Conceptcanonicalverified 2026-10-12

CONCEPT.TENANT-ISOLATION

How one company's data is kept from another's

Summary

Three layers, all in the database, so a mistake in application code cannot leak a company's data. Each is covered by a test in packages/db/src.

Why it matters

Trunk holds copies of its customers' products. The worst bug it can have is one company seeing another's.

Implications

  • Every row a company owns carries clientId.
  • A child's foreign key includes clientId, and productId where it has one. A page cannot belong to one company and point at another's product. A journey link cannot join pages of two products.
  • Row-level security hides other companies' rows. The app sets the company once per transaction with withClient(db, clientId, work). With nothing set, a query returns no rows.
  • The app connects as a role that owns nothing, so the rules apply to it. Migrations and system jobs connect as the owner. See .context/ops/setup.md.
  • Four tables can be read before a company is known: Client, Membership, Invite and PreviewLink. Anyone may read them; only the company may write them.
  • Optional links are checked by a trigger. A foreign key check ignores row-level security, so a trigger proves the linked row is in the same company.
  • Prisma drops a foreign key it did not write. So every foreign key is declared in schema.prisma. Checks, partial unique indexes, triggers and policies are hand-written at the bottom of the migration, and Prisma leaves them alone.
  • Not enforced by the database: that a company always has an owner. The code that removes a member must check it.
  • AI actions and invoices outlive what they point at. Deleting a product keeps its AI actions, so the meter cannot be reset. A company with invoices cannot be hard-deleted by accident.

Related

Sources

  • packages/db/prisma/schema.prisma
  • packages/db/prisma/migrations
  • packages/db/src/tenant.ts
  • packages/db/src/tenant.test.ts
  • packages/db/src/constraints.test.ts