Devyst
ProjectsBlogAboutContact
Devyst

A technology studio building AI products, custom software, and automation that helps your business grow globally.

Accepting New Clients
hello@devyst.com

Services

  • Agentic AI Systems
  • AI Chatbots
  • Custom SaaS
  • Workflow Automation
  • Full-Stack Development
  • AI Integrations
  • Social Media Marketing
  • Video Production

Company

  • About
  • Projects
  • Blog
  • Technologies
  • Contact

Connect

  • Twitter↗
  • GitHub↗
  • LinkedIn↗
  • hello@devyst.com
DEVYST
© 2026 Devyst. All rights reserved.Privacy Policy·Terms of Service
Home/Blog/Multi-Tenant SaaS Architecture with Next.js and NestJS
SaaS Architecture10 min readJanuary 22, 2025

Multi-Tenant SaaS Architecture with Next.js and NestJS

How to isolate tenants, model the database, and wire authentication so a single deployment safely serves many customers.

HS

Hira Saleem

Staff Engineer

Multi-TenantNext.jsNestJSPostgreSQLStripe

Introduction

Multi-tenancy 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, 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-neighbor 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.

Authentication

Authentication answers who the user is, while tenant resolution answers which organization the request acts on, and the two are distinct steps. A user may belong to several tenants, so the active tenant is usually carried in the token claims or derived from the subdomain and then verified against the user memberships. The critical rule is that the resolved tenant must be set into the request context before any data access happens, and every query downstream must respect it. Devyst implements this with a NestJS guard that runs on protected routes, extracts the tenant from the verified token, confirms the user has access, and attaches the tenant id to the request for repositories to consume. The guard below rejects any request whose token lacks a tenant claim or whose user is not a member of the requested tenant. Centralizing this logic in a guard means individual controllers never have to remember to check, which removes a whole class of mistakes.

typescript
import {
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common'
import { MembershipService } from './membership.service'

interface RequestUser {
  userId: string
  tenantId?: string
}

@Injectable()
export class TenantGuard implements CanActivate {
  constructor(private readonly memberships: MembershipService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest()
    const user: RequestUser | undefined = request.user

    if (!user || !user.tenantId) {
      throw new UnauthorizedException('Missing tenant context')
    }

    const hasAccess = await this.memberships.userBelongsToTenant(
      user.userId,
      user.tenantId,
    )

    if (!hasAccess) {
      throw new ForbiddenException('User is not a member of this tenant')
    }

    request.tenantId = user.tenantId
    return true
  }
}

Billing Integration with Stripe

Billing maps cleanly onto the tenant boundary because a tenant is almost always the billing entity, not an individual user. Each tenant links to a Stripe customer, and subscriptions, invoices, and payment methods all hang off that customer record. Devyst stores the Stripe customer id and subscription id on the tenant record and treats Stripe as the source of truth for entitlement state, syncing changes through webhooks rather than polling. Webhooks must be verified with the signing secret and processed idempotently, since Stripe can deliver the same event more than once and order is not guaranteed. Feature access then keys off the synced subscription status, so a downgrade or a failed payment promptly restricts what the tenant can do. Handle the dunning lifecycle explicitly, because a tenant in a past-due state needs a clear path back to good standing rather than a hard lockout that loses the account.

Always verify the Stripe webhook signature and make handlers idempotent. Duplicate events are normal and an unguarded handler can double-apply a billing change.

Deployment Considerations

A multi-tenant deployment shares infrastructure, so noisy neighbors and per-tenant limits move from theory to operations. Rate limiting should be scoped per tenant rather than globally, so one heavy account cannot exhaust capacity for everyone else. Database connection pooling needs care because a shared API can quickly exhaust PostgreSQL connections under load, which is where a pooler like PgBouncer earns its place. The async I/O work in PostgreSQL 18 makes heavy reads up to three times faster, but it does not change the connection math, so the pooler stays in the picture. Devyst runs the Next.js frontend and the NestJS API as separate deployable units so each scales on its own traffic profile, and it routes tenants by subdomain at the edge. Observability has to carry the tenant id on every log line and metric, otherwise diagnosing a tenant-specific issue becomes guesswork. Migrations also deserve a plan, since a schema change now affects every customer at once and a careful rollout with backward-compatible steps avoids downtime for the whole base.

  1. Introduction
  2. Tenant Isolation Strategies
  3. Database Design
  4. Authentication
  5. Billing Integration with Stripe
  6. Deployment Considerations

Related Articles

Next.js 16 Performance Patterns: App Router Optimization in Practice

Concrete techniques for shipping fast App Router applications, from bundle size to streaming to Core Web Vitals.

Adding AI to an Existing SaaS Product: An Engineering Playbook

How to introduce AI features into a live product without destabilizing it, blowing the budget, or shipping something nobody uses.

Tags

Multi-TenantNext.jsNestJSPostgreSQLStripe