Node.js Multi-Tenant SaaS: Shared vs Separate Database
Node.js multi-tenant SaaS architecture compared: shared schema, schema-per-tenant, and database-per-tenant, with Postgres RLS and AsyncLocalStorage patterns.
Node.js multi-tenant SaaS architecture compared: shared schema, schema-per-tenant, and database-per-tenant, with Postgres RLS and AsyncLocalStorage patterns.
If you're searching for "Node.js multi-tenant SaaS architecture," you're probably past the prototype stage and staring down a real decision: keep every customer's data in one shared database, or start splitting it apart. The short answer is that most SaaS teams should start with a shared database, shared schema, tenant_id column, and PostgreSQL row-level security (RLS) as a safety net, then peel off specific tenants into their own schema or database only when compliance, scale, or an enterprise contract demands it. The rest of this guide walks through why, and exactly how to implement each layer in a Node.js stack.
Every multi-tenant SaaS eventually chooses between three models, and the choice isn't permanent — most companies start at one end and migrate specific tenants toward the other as the business grows.
| Model | Isolation | Cost per tenant | Ops complexity | Best for |
|---|---|---|---|---|
| Shared DB, shared schema (tenant_id + RLS) | Logical, enforced by RLS | Lowest | Low — one schema to migrate | 0–10,000 SMB tenants |
| Shared DB, schema-per-tenant | Strong, namespace-level | Medium | Medium — migrations run N times | 100s of mid-market tenants |
| Database-per-tenant | Physical, maximum isolation | Highest | High — N connection pools, N backups | Enterprise, HIPAA/SOC 2 tenants |
A common mistake is treating this as a single, upfront architectural decision. In practice, the highest-leverage design is a hybrid: run the default tier on shared schema, and give your platform the ability to "promote" a specific tenant to its own schema or database without a rewrite. That optionality is worth more than picking the theoretically most scalable model on day one.
The riskiest failure mode in shared-schema multi-tenancy isn't a slow query — it's a developer forgetting a WHERE tenant_id = $1 clause and leaking one customer's data to another. Node.js gives you a clean way to make that mistake structurally hard to make: AsyncLocalStorage.
In practice this looks like a Postgres session variable (SET app.tenant_id = '...') set at the start of each transaction, with an RLS policy like USING (tenant_id = current_setting('app.tenant_id')::uuid) on every table. The application-layer filter and the RLS policy should agree, but RLS is what saves you the day someone ships a query without it.
Shared-schema tenancy pools connections cheaply because every tenant shares one Postgres connection pool. Schema-per-tenant and database-per-tenant break that assumption, and it's the part teams underestimate. A naive "one pool per tenant" design exhausts Postgres's max_connections long before you have a few hundred tenants, since Node.js processes are typically horizontally scaled across several instances.
Multi-tenancy doesn't stop at the database. Redis caches and session stores need the same discipline, or you'll cache tenant A's dashboard data under a key that tenant B's request can collide with. Namespace every cache key with the tenant ID (tenant:{id}:dashboard:summary), and if you're on Redis Cluster, let the tenant ID double as (part of) your hash tag so a given tenant's keys land on predictable slots — this keeps per-tenant cache invalidation ("clear everything for this tenant on plan downgrade") to a bounded SCAN instead of a full-keyspace sweep.
“Shared schema is safe if you make tenant filtering impossible to forget — not if you rely on every developer remembering to add it.”
Don't split tenants out speculatively — it multiplies operational surface area for no benefit until one of these triggers actually applies:
Build the promotion path (schema export → import into a dedicated database, with a background job to backfill and a cutover flag) once, early, as an internal tool — not as a one-off emergency migration the first time a $200,000/year enterprise deal requires it.
The three-model decision (shared schema, schema-per-tenant, database-per-tenant) is framework-agnostic — it's the same trade-off whether you're on Node.js or Laravel. What differs is the tooling: Laravel's ecosystem (stancl/tenancy, Eloquent global scopes) makes schema-switching more declarative, while Node.js leans on AsyncLocalStorage, Postgres RLS, and explicit middleware. If you're evaluating both stacks for a new SaaS build, our companion guide walks through the same decision from the Laravel side.
Comparing stacks for a new multi-tenant build? See how the same shared-vs-separate-database decision plays out on Laravel.
Read the Laravel multi-tenancy guideIt can be, if it's backed by PostgreSQL row-level security in addition to application-level filtering, and if you can point to that enforcement during a security review. Many SaaS vendors run shared schema for the vast majority of tenants and only promote specific accounts to dedicated infrastructure when a contract or regulation requires physical isolation.
AsyncLocalStorage removes the risk of a function forgetting to pass tenantId along, which is exactly the class of bug that causes cross-tenant leaks. It has a small performance cost, but for tenant context specifically the safety benefit outweighs it. Reserve explicit parameters for values that change the business logic itself, not for identity/context.
Postgres itself starts to strain when a single database holds many thousands of schemas, because catalog bloat slows down planning and tooling like pg_dump. Teams that reach that scale typically shard schema-per-tenant across several Postgres instances rather than pushing one instance past a few thousand schemas.
Set up logical replication (or a change-data-capture pipeline) from the shared database, filtered to that tenant's rows, into the new dedicated database. Let it catch up, then flip a feature flag that routes that tenant's connection string, verify writes on the new database, and only then decommission the old rows. Keep the cutover flag reversible for at least one release cycle.
Yes. Any shared cache, session store, or job queue needs tenant-prefixed keys and, on Redis Cluster, tenant-aware hash tagging — otherwise a cache key collision or an unscoped SCAN-based invalidation can expose or corrupt another tenant's cached data even when your database isolation is solid.
Multi-tenant architecture is a decision you'll revisit as the business grows, not a one-time diagram. The teams that handle it well design for the promotion path from the start, enforce isolation at more than one layer, and resist splitting tenants apart until a real trigger — compliance, contract, or contention — actually forces the question.
Planning a multi-tenant SaaS build on Node.js or evaluating your current isolation model?
Talk to Fepiq about your SaaS architectureOccasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.
Keep reading
What is an index in SQL? A plain-English guide with copy-paste examples showing how indexes speed up queries and when you actually need one.
Learn JavaScript DOM manipulation for beginners: select elements, change text and styles, and handle clicks with simple, copy-paste code examples.
Let's build something
Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.