Building Scalable SaaS Applications with Supabase
From auth to real-time subscriptions to multi-tenant RLS — a complete blueprint for building production SaaS on Supabase + Prisma.
Building Scalable SaaS with Supabase: The Complete Blueprint
Building a SaaS product used to mean assembling a dozen services: auth provider, database, file storage, real-time engine, email service. Supabase consolidates most of these into a single platform built on Postgres. Here's how I architect SaaS applications on Supabase, with the hard-won lessons from production.
Architecture Overview
A typical SaaS architecture on Supabase looks like this:
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Next.js │────▶│ Supabase │────▶│ PostgreSQL │
│ Frontend │ │ Auth + RLS │ │ (Managed) │
│ + API │ │ + Storage │ │ + Prisma ORM │
└─────────────┘ └──────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ Stripe │ │ Real-time │
│ Billing │ │ WebSocket │
└─────────────┘ └──────────────┘
The key insight: use Supabase for what it does best (auth, real-time, storage, RLS), but use Prisma for data access. This gives you type-safe queries while keeping Supabase's security model.
Authentication: The Right Way
Supabase Auth handles the heavy lifting — OAuth, magic links, MFA. But there's a critical pattern most tutorials miss: always verify tokens server-side with getUser(), never with getSession().
// ❌ WRONG: getSession() reads the JWT locally without validation
const { data: { session } } = await supabase.auth.getSession();
// ✅ CORRECT: getUser() validates the JWT against Supabase Auth server
const { data: { user } } = await supabase.auth.getUser();
Why? getSession() trusts the JWT in the cookie without verifying it. A tampered or expired token passes silently. getUser() makes a network call to validate — it's slower, but it's the only secure option for server-side auth decisions.
Multi-Tenant Row Level Security
RLS is Supabase's superpower for SaaS. Here's the pattern I use for multi-tenant data isolation:
-- Every table has a tenant_id column
ALTER TABLE projects ADD COLUMN tenant_id UUID REFERENCES tenants(id);
-- RLS policy: users can only access their tenant's data
CREATE POLICY "Tenant isolation" ON projects
FOR ALL USING (
tenant_id = (
SELECT tenant_id FROM tenant_members
WHERE user_id = auth.uid()
LIMIT 1
)
);
This is enforced at the database level — even if your API has a bug, data can never leak across tenants.
Real-Time Subscriptions
For features like live dashboards or collaborative editing, Supabase's real-time engine is invaluable:
const channel = supabase
.channel('orders')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'orders',
filter: `tenant_id=eq.${tenantId}`,
}, (payload) => {
addOrder(payload.new);
})
.subscribe();
Performance tip: Always filter by tenant_id in your subscription. Without a filter, every client receives every change — a recipe for bandwidth explosion.
Pricing & Billing with Stripe
I integrate Stripe for subscription management. The key architecture decision: store the subscription state in your database, not just in Stripe:
model Tenant {
id String @id @default(cuid())
name String
stripeCustomerId String? @unique
subscriptionStatus String @default("trialing")
planId String @default("free")
currentPeriodEnd DateTime?
}
Stripe webhooks keep this in sync. Your application reads from your database (fast, no API calls), and Stripe is the source of truth for billing.
Deployment & Scaling
Supabase handles Postgres scaling up to 64 cores and 256GB RAM on their Pro plan. For most SaaS products, you'll hit frontend bottlenecks long before database bottlenecks.
Key Scaling Strategies
- Connection Pooling: Use Supabase's built-in PgBouncer for connection pooling
- Read Replicas: Available on Pro plan for read-heavy workloads
- Edge Functions: For compute that needs to be close to users
- CDN: Vercel's edge network for static assets and ISR pages
Lessons from Production
- Start with the free tier. Supabase's free tier is generous enough for MVP and early customers.
- Invest in RLS from day one. Retrofitting RLS into an existing app is painful.
- Use Prisma migrations, not Supabase Studio. Version-controlled, reproducible, and reviewable.
- Monitor your connection count. Serverless functions can exhaust connections quickly — use pooling.
- Test your RLS policies. Write integration tests that verify tenant isolation actually works.
Conclusion
Supabase + Prisma + Next.js is the most productive SaaS stack I've used. It gives you enterprise-grade features (auth, RLS, real-time) without enterprise-grade complexity. The key is understanding where each tool shines and using them together thoughtfully.