Multi-Tenant SaaS with Next.js, Prisma, and Stripe in 2026
Next.jsBackendTypeScript

Multi-Tenant SaaS with Next.js, Prisma, and Stripe in 2026

Architecture decisions for a multi-tenant SaaS in Next.js 14 — schema-per-tenant vs row-level isolation, Prisma patterns, Stripe Connect vs Stripe Billing, and the pitfalls.

HJ
Hassan Javed
December 2025
11 min read

What I'm building toward

I've shipped three multi-tenant SaaS apps on Next.js plus Prisma plus Stripe in the last year. The architecture decisions made on day one drive what's possible (and what's painful) for years after.

This post is the canonical pattern I now reach for, with the trade-offs that bit me on the first two before I settled on the third.

Tenant isolation: three options

The single biggest architectural choice. Three real options:

1. Row-level isolation (single database, tenantId on every row)

All tenants share one Postgres database. Every table has a tenantId column. Every query is filtered by tenantId.

Pros: Simplest operations. One DB to back up, one DB to migrate. Cheapest at scale.

Cons: A bug that forgets to filter by tenantId leaks data. You need Prisma middleware or a custom client wrapper to enforce filtering.

Use when: Most SaaS in 2026. The simplicity wins.

2. Schema-per-tenant (one DB, separate schemas)

All tenants share one Postgres instance, but each has its own schema. Queries automatically scope to the tenant's schema.

Pros: Strong isolation. Per-tenant migrations possible. Cleaner mental model.

Cons: Schema migrations across hundreds of tenants are operational pain. Prisma's schema-per-tenant story is still rough.

Use when: Healthcare, finance, anywhere you need true isolation for compliance.

3. Database-per-tenant

Each tenant gets their own Postgres instance.

Pros: Maximum isolation. Per-tenant backup or restore. Per-tenant performance tuning.

Cons: Operational nightmare. Migrations across hundreds of DBs is hard. Expensive.

Use when: Enterprise clients with regulatory requirements that demand it.

My default in 2026: row-level

For 95 percent of SaaS projects, I do row-level isolation with strict enforcement at the data layer.

The enforcement pattern:

1.Authentication middleware extracts tenantId from session
2.All Prisma queries go through a wrapper that injects tenantId into the where clause
3.Tests verify cross-tenant access is impossible (try to read another tenant's data and confirm it returns nothing)

I use Prisma's $extends API for the wrapper. About 50 lines of code, prevents an entire class of bugs.

Prisma schema design

A multi-tenant schema typically has these tables:

Tenant (or Workspace, Org) — the tenant entity
User — users can belong to multiple tenants via memberships
Membership — joins User to Tenant with a role
Resource tables — every domain table has tenantId column

The User-Membership-Tenant triangle is the foundation. Users can switch between tenants without re-logging in. Their session holds the active tenantId.

Authorization: roles and permissions

Two-level model:

1.Membership role (admin, editor, viewer) — coarse-grained, per-tenant
2.Resource permissions — fine-grained, e.g., "can edit this specific project"

For most SaaS, role-based is enough. Reach for resource-level only when you need it.

Implementation: simple enum on Membership table for roles. For resource-level, a Permission table that joins User, Resource, and PermissionType.

Stripe: Billing vs Connect

Two completely different products from Stripe; I see teams confuse them.

Stripe Billing

For: charging your customers (tenants) on subscriptions. Subscription management, invoicing, payment retries, dunning emails, customer portal. This is what you want for SaaS.

Stripe Connect

For: enabling your customers to charge their own customers via your platform. Marketplaces, multi-vendor platforms. Your platform takes a fee; the rest goes to your customer.

For 95 percent of SaaS, you want Billing. Connect is for marketplace business models.

Stripe Billing integration patterns

What I do for every SaaS:

1.Subscription = Tenant's plan. One Stripe Subscription per Tenant. The Subscription's metadata.tenantId links to your DB.
2.Customer = Tenant's billing entity. One Stripe Customer per Tenant.
3.Webhook handler for subscription events (created, updated, deleted, payment_failed). Updates your Tenant.plan field.
4.Customer Portal (Stripe-hosted) for users to manage their billing. Massively reduces support burden.
5.Usage-based billing via meter events for plans that have it.

The webhook gotcha

Stripe webhooks are at-least-once delivery. You'll receive duplicates. Make sure your handlers are idempotent. Use event.id as a dedupe key — if you've processed it, skip.

Plans, limits, and feature flags

For tier-gating:

Plan definitions in code (not DB) — easier to evolve, version-control diffs.
Tenant.plan in DB stores the current plan slug ("free", "pro", "team").
Helper function to check feature access: hasFeature(tenant, "advanced-analytics").
Soft limits (warn user) and hard limits (block action) — clearly documented per plan.

Domain handling

Each tenant gets a subdomain (acme.yourapp.com) or custom domain (app.acmecorp.com).

For subdomains: middleware extracts the subdomain, looks up the tenant, attaches to request context.

For custom domains: client adds a CNAME to their domain pointing to your app's domain. Your app uses the request's Host header to look up the tenant. Caddy or Vercel handles SSL automatically.

Onboarding flow

The flow that works:

1.Sign up — user creates account, no tenant yet
2.Create first workspace — name, slug, etc.
3.Pick a plan (or start free trial)
4.Add billing info (optional for free trial)
5.Invite team (optional, can skip)
6.Land on dashboard

Each step is its own page; can be paused and resumed. Don't put everything on one form.

What I regret in past projects

Two specific mistakes:

1. Built fine-grained permissions from day one

Spent two weeks building a per-resource permission system. Customers used three roles total for three years. Massive waste.

2. Used schema-per-tenant for "isolation"

Got isolation. Lost ergonomics. Every migration was a multi-step deploy across N tenants. Eventually migrated to row-level. Two months of work for negative business value.

My stack for new SaaS in 2026

Next.js 14 App Router with TypeScript
Prisma with row-level tenant isolation enforced via extends
PostgreSQL (Supabase or Neon for hosting)
NextAuth v5 for auth (or Clerk if budget allows)
Stripe Billing for subscriptions
Resend for transactional email (Stripe handles billing emails; everything else is mine)
Vercel for deployment
Sentry for error tracking
PostHog for product analytics

Estimated build effort

For a v1 multi-tenant SaaS with auth, billing, dashboard, and one core feature: 4-6 weeks of focused engineering. Add core domain features on top.

TL;DR

Row-level isolation for 95 percent of SaaS, schema-per-tenant only for compliance, DB-per-tenant for enterprise
Prisma extends for tenant-filter enforcement
Stripe Billing for SaaS subscriptions, not Connect (unless marketplace)
Webhook handlers must be idempotent (deduplicate by event.id)
Don't over-engineer permissions on day one

If you're starting a multi-tenant SaaS and want a senior to architect the foundation correctly, contact me.

Related Reads

You might also like