Multi-tenant SaaS boilerplate with Next.js, Prisma and tRPC
Multi-tenant,
not just promised. Backed by tests.
Most boilerplates add an organizationId column and leave the filtering to you. HeartCo automates reads, keeps writes to one explicit rule, and ships the tests that prove it.
How the isolation works
One Prisma extension for reads, one simple rule for writes.
Reads, filtered automatically
ctx.orgDb adds organizationId to findMany, findFirst, findFirstOrThrow, count, aggregate and groupBy. Nothing to write, nothing to forget.
Writes stay explicit, on purpose
create, update and delete are not covered by the automatic filter: organizationId has to appear in the where clause by hand, easy to check in review.
findFirst, never findUnique
findUnique only accepts unique fields in its where clause, so the org filter cannot be added to it. One rule, easy to grep for.
Sub-entities through the parent
An invoice line has no organizationId of its own: the filter goes through the relation to the invoice, never through the line's ID alone.
IDOR: NOT_FOUND, never FORBIDDEN
A resource from another organization comes back as missing, not refused. A refusal confirms a valid ID exists somewhere else.
RBAC as a matrix
Isolation protects the boundary between organizations. A per-role permission matrix protects the boundaries inside one.
What the isolation covers, and what it doesn't
Most product pages stop at the first table. Here's the second one.
What ctx.orgDb filters
- Reads on models registered as organization-scoped
- findMany, findFirst, findFirstOrThrow, count, aggregate, groupBy
- The same way in every router that uses ctx.orgDb
What stays on you
- Every write: create, update, delete
- findUnique, deliberately left out of the automatic filter
- Registering a new model as organization-scoped
- Direct access through ctx.db (webhooks, scheduled jobs, admin tooling)
The tests that ship, not a promise
Twelve security test files that come with the code, not a checklist you write yourself.
Cross-tenant isolation
Two mocked organizations, every sensitive read (invoices, quotes, conversations) checked on its own.
IDOR by domain
One dedicated file per sensitive surface: accounting, conversations, messages, notifications, third-party integration events.
An audit that walks the schema
One generic test checks that every scoped model goes through the automatic filter, not just the models already covered by hand.
Who this is for, and who it isn't
- You're building a B2B SaaS where one organization has several users
- A data leak between two customers would be an incident, not a footnote
- You want tests that ship with the code, not an architecture to design alone
- Your product is single-tenant, with no notion of an organization
- You'd rather isolate with a separate database per customer
The isolation code, not just the pitch.
Prisma, tRPC, the security tests: all in the repo, from day one.