Back to Blog
·14 min

SaaS Architecture Patterns with Next.js: Multi-Tenant, Billing, and Team Management

Building a production SaaS application with Next.js requires careful architectural decisions across tenancy, billing, authentication, and data isolation. This guide walks through proven patterns used by successful SaaS startups.

Multi-Tenant Data Architecture

The most common SaaS architecture pattern is shared-database with tenant isolation via a tenantId column. This balances cost efficiency with data separation.

// middleware.ts - extract tenant from subdomain or session
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const host = request.headers.get('host') || ''
  const subdomain = host.split('.')[0]
  const response = NextResponse.next()
  response.headers.set('x-tenant-id', subdomain)
  return response
}

Every database query should include a tenantId filter. Using Prisma, create a middleware that automatically scopes queries. This prevents accidental cross-tenant data access.

Subscription Billing with Stripe

The billing layer handles plan selection, subscription lifecycle, usage metering, and webhook processing. Key webhook events to handle include checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed, and invoice.paid.

// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe'
import { headers } from 'next/headers'
import { NextResponse } from 'next/server'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: Request) {
  const body = await req.text()
  const signature = headers().get('stripe-signature')!
  const event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)

  switch (event.type) {
    case 'customer.subscription.updated':
      await syncSubscriptionStatus(event.data.object)
      break
    case 'invoice.payment_failed':
      await notifyPaymentFailure(event.data.object)
      break
  }
  return NextResponse.json({ received: true })
}

Team Management and RBAC

SaaS products need team workspaces with role-based access control. Define roles like owner, admin, member, and viewer, each with specific permissions mapped to route access.

type Role = 'owner' | 'admin' | 'member' | 'viewer'

const permissions: Record<Role, string[]> = {
  owner: ['*'],
  admin: ['manage_team', 'manage_billing', 'manage_settings'],
  member: ['create_project', 'edit_project'],
  viewer: ['view_projects'],
}

export function hasPermission(role: Role, permission: string): boolean {
  return permissions[role].includes('*') || permissions[role].includes(permission)
}

Rate Limiting and Usage Metering

Track API usage per tenant to enforce plan limits and enable usage-based billing. Store usage counters in Redis for fast reads and periodic aggregation to your database for billing.

Scaling Considerations

As your SaaS grows, plan for horizontal scaling with serverless functions, edge caching, and database connection pooling. Use Vercel for deployment and Upstash Redis for rate limiting and session storage.

If you are building a SaaS product, the SaaS Starter Kit from BreafIO provides all of these patterns pre-implemented with Next.js, Prisma, Stripe, and NextAuth. For team management, the Admin Dashboard Pro includes a complete RBAC system. For simpler billing, check out the Subscription Hub. The API Boilerplate is great for backend-only services, and the Landing Page Bundle helps you launch your marketing site.

Ready to Build?

Get started with our production-ready starter kits and ship your project faster.

Browse Starter Kits