Architecting Enterprise-Grade Multi-Tenant SaaS with Next.js and Supabase

A complete technical blueprint for B2B SaaS builders. Learn how to implement secure database isolation with Supabase RLS, optimize connection pooling, and handle dynamic subdomains in Next.js.

Architecting Enterprise-Grade Multi-Tenant SaaS with Next.js and Supabase: A Blueprint for Secure Data Isolation, Connection Pooling, and Dynamic Tenant Routing

Building a software-as-a-service (SaaS) application targeting US/UK enterprises and mid-market organizations requires meeting strict security, performance, and compliance standards. A key architectural decision is how to handle multi-tenancy.

In this deep dive, we walk through the exact blueprint for building a multi-tenant SaaS application using Next.js and Supabase. We will explore database-level tenant isolation, dynamic subdomain routing, and performance optimization techniques that will scale from your first customer to enterprise-grade workloads.


1. Defining the Core Multi-Tenancy Models

When designing a database for B2B SaaS, developers generally choose between three levels of isolation:

Isolation ModelImplementationSecurity LevelMaintenance OverheadCost Efficiency
Shared Database, Shared Schema (RLS)Tenancy isolated via row policies in a single schema.High (when configured correctly)Very LowExcellent
Shared Database, Separate SchemasA PostgreSQL schema per tenant in a single database.Very HighMediumGood
Separate DatabasesA dedicated physical database server per tenant.AbsoluteHighPoor

For 95% of B2B SaaS workloads, the Shared Database, Shared Schema model utilizing PostgreSQL's Row-Level Security (RLS) is the optimal choice. It provides strong logical data isolation while keeping server costs minimal and schema updates simple.


2. Implementing Row-Level Security (RLS) in Supabase

To implement this model, every tenant-scoped table must include a tenant_id column. We will set up a robust system where database transactions are automatically restricted to the active tenant.

Let's start by creating the database tables and configuration.

sql-- Create tenants table to hold organization profiles
CREATE TABLE public.tenants (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,
    subdomain TEXT UNIQUE NOT NULL,
    custom_domain TEXT UNIQUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);

-- Associate users with tenants (Many-to-Many or One-to-Many junction)
CREATE TABLE public.tenant_users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID REFERENCES public.tenants(id) ON DELETE CASCADE NOT NULL,
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
    role TEXT CHECK (role IN ('owner', 'admin', 'member')) DEFAULT 'member' NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
    UNIQUE(tenant_id, user_id)
);

-- Sample tenant-scoped data table
CREATE TABLE public.documents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID REFERENCES public.tenants(id) ON DELETE CASCADE NOT NULL,
    title TEXT NOT NULL,
    content TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);

The RLS Trap and How to Fix It

By default, developers write RLS policies that query other tables, like this: USING (auth.uid() IN (SELECT user_id FROM tenant_users WHERE tenant_id = documents.tenant_id))

Why is this a performance trap? Every single row evaluation executes a subquery, resulting in an O(N) complexity for table scans. For a table of 100,000 rows, this can degrade performance rapidly.

The Solution: Use Supabase JWT custom claims or a lightweight helper function that caches the user's active tenant IDs in database session variables or reads them directly from the JWT. Here is a secure, index-friendly implementation:

sql-- Enable RLS on all tenant-scoped tables
ALTER TABLE public.tenants ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.tenant_users ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;

-- Dynamic helper function to fetch active tenant context
CREATE OR REPLACE FUNCTION public.current_tenant_id()
RETURNS UUID AS $$
  -- Reads from database request config set during server client init
  SELECT nullif(current_setting('request.jwt.claims', true)::json->>'tenant_id', '')::uuid;
$$ LANGUAGE sql STABLE;

-- RLS Policy for Documents
CREATE POLICY "Users can access documents belonging to their tenant"
ON public.documents
FOR ALL
TO authenticated
USING (
  tenant_id = public.current_tenant_id()
);

3. Dynamic Subdomain and Domain Routing in Next.js

Once database isolation is secured, we must route incoming traffic dynamically. If a user visits acme.your-platform.com or custom-domain.com, your Next.js application must serve their specific workspace.

Configure Next.js Middleware for Subdomain Resolution

We can rewrite the requests dynamically in the middleware.ts file to match a tenant-based file path inside the Next.js App Router (e.g., /src/app/[tenant]/...).

typescriptimport { NextRequest, NextResponse } from "next/server";

export function middleware(req: NextRequest) {
  const url = req.nextUrl;
  const hostname = req.headers.get("host") || "";

  // Exclude system static paths or API routes
  if (
    url.pathname.startsWith("/_next") ||
    url.pathname.startsWith("/api") ||
    url.pathname.includes(".")
  ) {
    return NextResponse.next();
  }

  // Parse host to extract subdomain
  // e.g., acme.yourplatform.com -> acme
  const isLocalhost = hostname.includes("localhost");
  const baseDomain = isLocalhost ? "localhost:3000" : "yourplatform.com";
  
  let subdomain = "";
  if (hostname.endsWith(baseDomain)) {
    subdomain = hostname.replace(`.${baseDomain}`, "").replace(baseDomain, "");
  }

  // Rewrite matching paths to the dynamic tenant group
  if (subdomain && subdomain !== "www") {
    url.pathname = `/[tenant]${url.pathname}`;
    const response = NextResponse.rewrite(url);
    // Set custom header to pass the resolved tenant to the app
    response.headers.set("x-resolved-tenant", subdomain);
    return response;
  }

  return NextResponse.next();
}

4. Frequently Asked Questions

Let's address some common architectural questions when building multi-tenant SaaS platforms.

How does Row-Level Security (RLS) impact query performance in Supabase?

Row-Level Security (RLS) appends policy logic directly to SQL statements, meaning query complexity depends on policy efficiency. To keep performance high, ensure any foreign keys or columns used in your RLS policies (such as tenant_id) are indexed. Avoid deep joins or subqueries within policies; instead, utilize Supabase JWT custom claims or session context parameters.

What is the difference between single-database RLS and schema-per-tenant?

Single-database RLS handles isolation logically, storing all tenant data on shared tables filtered by a tenant_id column and secured via database policies. This is extremely cost-effective, straightforward to scale, and simplifies database migrations. In contrast, schema-per-tenant creates a separate PostgreSQL schema for each customer. While this offers higher physical isolation, it increases operational complexity and costs, making schema migrations difficult at scale.

How should connection pooling be configured for B2B SaaS workloads?

B2B workloads are often bursty, especially during business hours, which can exhaust database connection pools. For serverless infrastructures like Next.js API routes or Server Components, configure Supabase's built-in pooler (Supavisor) in Transaction Mode. This ensures connections are released back to the pool as soon as the database query completes, rather than keeping them open during the entire serverless function execution lifecycle.


5. Strategic Takeaways

Building a B2B SaaS with Next.js and Supabase provides a powerful combination of developer productivity and robust security.

  • Always write indexable, subquery-free RLS policies to prevent table scan lag.
  • Implement Next.js Middleware rewrites to offer custom subdomains or fully customized white-label domains.
  • Use transaction-mode connection pooling to handle concurrent enterprise users without running into database limits.

This guide contains everything you need to build a secure, high-conversion multi-tenant application that stands up to enterprise scrutiny.

Ready to Build?

Need this architecture implemented for your SaaS?

I help US & UK SaaS founders ship production-grade systems — multi-tenant architectures, workflow engines, and custom platforms — without the guesswork.

Schedule Free ConsultationView My Work