Introduction
Multitenancy means one running application serves many customer organizations while keeping each tenant data strictly separated. The appeal is operational: one deployment to patch, one database to back up, and shared infrastructure cost across every account. The risk is that a single mistake in a query or a guard can leak one customer data to another, which is the most damaging bug a SaaS can ship. Devyst treats the tenant boundary as the central security invariant of the whole system, enforced in the data layer rather than trusted to application code alone. This guide uses Next.js for the frontend and NestJS for the API, the stack behind most custom SaaS work at Devyst, and it focuses on the decisions that are expensive to change after launch. The goal is a design where isolation is the default and a developer has to work hard to break it, rather than the reverse.
Tenant Isolation Strategies
There are three common isolation models, and the choice shapes everything downstream. A separate database per tenant gives the strongest isolation and the simplest backup story, but it scales poorly past a few hundred tenants because migrations and connection pools multiply. A shared database with a separate schema per tenant sits in the middle, offering decent isolation with more manageable operations. A shared schema with a tenant id column on every row scales to large tenant counts and keeps operations simple, at the cost of relying on disciplined filtering to keep tenants apart. Devyst defaults to the shared schema model for most products and enforces the boundary with PostgreSQL row level security so the database itself rejects cross tenant reads. The decision is rarely permanent in spirit but is expensive in practice, so it deserves a deliberate review of expected tenant count, compliance requirements, and per tenant data volume before any code is written.
Row level security in PostgreSQL lets the database enforce the tenant boundary even when application code has a bug. Set the tenant context per connection and let policies do the filtering.
Database Design
In a shared schema model, a tenant id belongs on every table that holds tenant owned data, and it should be part of the primary or composite indexes that queries rely on. Foreign keys must stay within a tenant, so a child row should never reference a parent that belongs to a different organization, a constraint worth enforcing in schema design and tests. Devyst adds a not null tenant id to every such table and creates composite indexes that lead with tenant id, since nearly every query filters on it first. Soft deletes and audit columns are easier to add at the start than to retrofit, so include created, updated, and deleted timestamps from the beginning. Primary keys deserve a moment of thought too: PostgreSQL 18 ships a native uuidv7() function, which gives you time ordered ids that index well without pulling in an extension. Plan for the noisy neighbour problem as well, where one large tenant degrades performance for everyone, and consider partitioning the heaviest tables by tenant id once volume justifies it. A clean schema here pays off because every later feature inherits its isolation guarantees for free, which is what made the audit trail on the Ledgerly billing platform a query rather than a rebuild.
