All posts
Node.jsAugust 12, 20269 min read

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.

F
Fepiq Team
Fepiq

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.

The three multi-tenancy models

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.

ModelIsolationCost per tenantOps complexityBest for
Shared DB, shared schema (tenant_id + RLS)Logical, enforced by RLSLowestLow — one schema to migrate0–10,000 SMB tenants
Shared DB, schema-per-tenantStrong, namespace-levelMediumMedium — migrations run N times100s of mid-market tenants
Database-per-tenantPhysical, maximum isolationHighestHigh — N connection pools, N backupsEnterprise, 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.

Enforcing tenant isolation in a Node.js request lifecycle

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.

  • Resolve the tenant once, in middleware, from the JWT claim or subdomain — never from a client-supplied body or query parameter.
  • Store it in an AsyncLocalStorage context so every service, repository, and background job in that request's async chain can read it without threading a tenantId parameter through every function signature.
  • Set it per transaction, not per connection. A pooled connection is reused across requests; binding tenant context to the connection instead of the transaction is a well-documented way multi-tenant apps have leaked data in production.
  • Enable PostgreSQL RLS on every tenant-scoped table as a second, database-level enforcement layer — so even if application code forgets the filter, Postgres refuses to return rows outside the current tenant.

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.

Connection pooling at scale

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.

  1. Put PgBouncer (or RDS Proxy on AWS) in transaction-pooling mode between your Node.js services and Postgres, so hundreds of logical tenant connections multiplex onto a small number of real backend connections.
  2. For schema-per-tenant, switch the active schema per transaction with SET search_path, reusing the same pooled connection rather than opening a dedicated one.
  3. For database-per-tenant, cap concurrent pools with an LRU eviction strategy — keep hot tenants' pools warm and let idle tenants' pools close, rather than holding one open per tenant forever.
  4. Run migrations through a queue, not a loop in the deploy script — schema-per-tenant means running the same migration N times, and a mid-run failure on tenant 214 of 900 needs to be resumable, not a full rollback.

Tenant-aware caching with Redis

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.
Common wisdom among teams running RLS-backed multi-tenant Postgres in production

When to promote a tenant off the shared schema

Don't split tenants out speculatively — it multiplies operational surface area for no benefit until one of these triggers actually applies:

  • A compliance requirement (HIPAA, SOC 2 Type II, FINRA) contractually requires physical data isolation for that customer.
  • A single tenant's write volume or table size is degrading query plans or autovacuum behavior for every other tenant sharing the table.
  • An enterprise contract explicitly requires a dedicated database, region, or backup schedule as a sales term.
  • You need to offer a tenant a different Postgres version, extension set, or maintenance window than the rest of your fleet.

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.

How this compares to Laravel multi-tenancy

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 guide

A pragmatic starting checklist

  • Start shared schema with a tenant_id column, a composite index on (tenant_id, id), and RLS enabled from day one — retrofitting RLS onto a live table is far more painful than adding it up front.
  • Resolve tenant context in middleware from a verified JWT claim, store it in AsyncLocalStorage, and set it per transaction.
  • Namespace Redis keys and background job payloads by tenant ID so caching and queues don't become the leak vector once the database is locked down.
  • Put PgBouncer or RDS Proxy in front of Postgres before you hit connection limits, not after an incident.
  • Build the tenant-promotion path once, before your first enterprise contract requires it.

Frequently asked questions

Is shared-schema multi-tenancy safe enough for enterprise customers?+

It 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.

Should I use AsyncLocalStorage or pass tenantId explicitly through every function?+

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.

When does schema-per-tenant stop making sense in PostgreSQL?+

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.

How do I migrate a tenant from shared schema to its own database with zero downtime?+

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.

Does Redis need to be multi-tenant aware too, or just the database?+

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 architecture

Get new posts in your inbox

Occasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.

Keep reading

Related posts

All posts

Let's build something

Ready to ship your next product with Fepiq?

Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.