SaaSMay 6, 20268 min read

Multi-Tenant SaaS Architecture with Supabase and Prisma

Nexus is a multi-tenant SaaS platform for wellness businesses. Every merchant gets their own storefront, dashboard, and data — but it all runs on a single PostgreSQL database. Here's how we architected it.

Single database, complete isolation

The most common question in multi-tenant architecture: separate databases per tenant, or shared database with tenant scoping? We chose shared database with row-level isolation. Here's why.

Separate databases sound clean in theory but create operational nightmares. Every migration needs to run across every database. Connection pooling becomes complex. Backups multiply. And costs scale linearly with tenant count, not usage.

Instead, every table in Nexus has a tenantId column. Every query is scoped. The Express middleware extracts the tenant from the request (via subdomain, custom domain lookup, or auth token) and injects it into the request context. Prisma queries are then scoped automatically.

The middleware chain

Every API request passes through three middleware layers:

  • Tenant resolution. Extracts the tenant slug from the subdomain or looks up the custom domain in a domain mapping table. Caches the result in Redis so subsequent requests skip the DB lookup.
  • Authentication. Validates the Supabase JWT and loads user + role data. Role determines what the user can access within the tenant.
  • Error handler. Catches everything, logs context (tenant, user, route), and returns clean error responses. Sensitive data never leaks in error messages.

Payment gateway isolation

This is where most tutorials stop and real production starts. Every tenant has their own Razorpay and/or Stripe credentials. These keys are encrypted at rest using AES-256 and decrypted only when processing a payment. The platform never touches merchant money — payments go directly from customer to merchant via their own gateway account.

Webhooks are routed per-tenant via the URL pattern: /api/webhooks/razorpay/:tenantSlug. The middleware resolves the tenant from the slug, decrypts their webhook secret, validates the signature, and processes the event — all before any business logic runs.

The Turborepo structure

Nexus is a monorepo with three applications and shared packages:

  • apps/api — Express + TypeScript + Prisma. Handles all business logic, payment processing, and webhook handling.
  • apps/dashboard — Next.js 14 merchant admin. Products, orders, bookings, staff, analytics, settings.
  • apps/storefront — Next.js 14 public tenant storefront. Product listing, booking flow, checkout.

All three share TypeScript types, Zod validation schemas, and utility functions through npm workspaces. Turborepo handles build orchestration and caching — if the API types change, both frontends rebuild. If only the dashboard changes, the API and storefront are skipped.

Custom domains

Every tenant starts with a subdomain like studio-name.nexus.app. When they upgrade, they can add a custom domain. The flow:

Merchant adds their domain in settings → we verify DNS (CNAME pointing to our edge) → Cloudflare issues an SSL certificate → the domain mapping is stored in the database and cached in Redis. The storefront reads the hostname on every request and resolves the tenant accordingly.

What I'd do differently

If starting over, I'd use Supabase Row Level Security (RLS) policies more aggressively instead of relying purely on middleware-level scoping. RLS provides a database-level safety net — even if a middleware bug leaks, the database won't return data from the wrong tenant. We added RLS retroactively, but designing for it from day one would have been cleaner.

I'd also choose Redis for session storage from the start instead of adding it later. Supabase Auth handles the JWT, but tenant context caching was an afterthought that caused unnecessary latency in the first few weeks.

Building a multi-tenant SaaS? Let's talk on WhatsApp or check out the full Nexus case study.