Postgres-first, RLS-native architecture
Multi-tenancy is the hardest problem in SaaS. Most studios bolt on tenant isolation after the fact — middleware guards, application-level filters, fragile ownership checks. We solve it at the database layer from day one using Supabase's Row Level Security. When a query runs, Postgres enforces the tenant boundary before a single row is returned. There is no code path that bypasses it.
Supabase gives us a full production backend — Auth, Storage, Realtime, Edge Functions, and the Management API — that integrates natively with our Next.js App Router stack. The result is a cohesive architecture with fewer seams and significantly less infrastructure to manage.
Row Level Security
Every query filtered at the database layer. Multi-tenant isolation without application-level guards — RLS policies are the boundary, not an afterthought.
Realtime
Postgres changes streamed to connected clients over WebSockets. Powers live dashboards, case-update feeds, and event check-in views without a separate pub/sub layer.
Auth + JWT
Built-in user management with custom JWT claims for role-based access. Firm invite flows, role elevation, and session management handled without third-party services.
Storage
Object storage with RLS-extended access control — the same policy layer that governs rows governs file access. Legal documents, event media, and CNIC docs in one stack.
Edge Functions
Deno-native serverless functions at the database edge. Used for payment webhooks, court deadline calculations, and HMAC-SHA256 QR key seeding without cold-start latency.
Management API
Programmatic project provisioning and schema migrations via Supabase's Management API — enabling repeatable, auditable infrastructure across environments.
Live SaaS built on Supabase
Two Bit Digital has shipped two production SaaS platforms using Supabase as the primary backend. Both are multi-tenant from the ground up, with all isolation enforced at the database layer via RLS.
Standard Two Bit Digital stack
All production SaaS platforms follow the same architectural spine: Next.js App Router on the front end, Supabase as the primary backend, deployed on Vercel with optional AWS for compute-heavy workloads. RLS is defined first, schema second, application code third — never the other way around.
RLS Policy Pattern
-- Example: firm-scoped Row Level Security for a legal case management system
-- All tenant isolation happens at the Postgres layer — not in application code.
alter table cases enable row level security;
create policy "Users see only their firm's cases"
on cases for select
using (
firm_id = (auth.jwt() ->> 'firm_id')::uuid
);
create policy "Fee earners can insert cases for their firm"
on cases for insert
with check (
firm_id = (auth.jwt() ->> 'firm_id')::uuid
and (auth.jwt() ->> 'role') in ('fee_earner', 'costs_lawyer', 'partner')
);
-- JWT payload injected at login via Supabase Auth hook:
-- { "firm_id": "uuid", "role": "costs_lawyer", "user_id": "uuid" }Supabase Client — Next.js App Router
// lib/supabase/server.ts — App Router server-side client
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import type { Database } from '@/types/supabase'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
},
},
}
)
}Getting started with our stack
The core integration is straightforward. The opinionated part is the RLS-first methodology — every table has RLS enabled before any application code touches it.
1. Install dependencies
npm install @supabase/supabase-js @supabase/ssr2. Configure environment variables
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# Server-side only — never expose to the client
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key3. RLS-first principles
Enable RLS before everything else
Run `alter table <name> enable row level security;` as the first migration on every new table. No exceptions.
Inject tenant context via JWT
Use a Supabase Auth hook to inject firm_id, org_id, or tenant_id into the JWT at login. Policies reference auth.jwt() directly.
Default-deny, explicit-allow
With RLS enabled and no policies defined, all access is denied. Add policies only for the operations each role legitimately needs.
Extend RLS to Storage
Storage bucket policies mirror your table policies. A user who cannot read a case record cannot read the documents associated with it either.
Work with Two Bit Digital
Ready to build a production SaaS platform on Supabase? We'd love to hear about your project.