
Multi-tenancy means multiple customers (tenants) share the same application instance and infrastructure while each believing they have a dedicated, isolated environment. Getting this architecture right is the difference between a SaaS that scales to 500 customers without incident and one that suffers data leaks, performance degradation, or billing errors as it grows.
There are three primary multi-tenancy models:
Each customer gets their own PostgreSQL database. Complete isolation. Zero risk of cross-tenant data leakage.
Pros: Maximum isolation, easy per-tenant backups, simple regulatory compliance Cons: Expensive at scale (each DB has fixed costs), complex schema migrations require running against every tenant DB, difficult to run cross-tenant analytics
Best for: High-security sectors (finance, healthcare) or very high-value enterprise clients.
One database, with each tenant getting their own PostgreSQL schema (namespace). Tables are isolated but the database engine is shared.
Pros: Better isolation than single-schema, easier to implement per-tenant migrations Cons: Moderate complexity, schema count can affect performance at high tenant counts
Best for: Medium-scale SaaS with strict data isolation requirements.
All tenants share the same tables, with a tenant_id column on every table. PostgreSQL Row-Level Security (RLS) policies enforce that queries only return rows belonging to the current tenant.
Pros: Most operationally efficient, easiest to run cross-tenant analytics, lowest infrastructure cost Cons: A misconfigured RLS policy could leak cross-tenant data (mitigated by thorough testing)
This is the model we use for most SaaS MVPs — it's battle-tested, cost-efficient, and PostgreSQL's RLS is robust.
-- Enable RLS on the table
ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;
-- Create policy: tenants can only see their own rows
CREATE POLICY tenant_isolation_policy ON subscriptions
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
-- In your application code, set the tenant context per request
-- (Prisma middleware example)
prisma.$use(async (params, next) => {
await prisma.$executeRaw`SET app.current_tenant_id = ${tenantId}`;
return next(params);
});
Each tenant maps to one Stripe Customer. Your database stores:
tenant_id → your internal IDstripe_customer_id → Stripe Customer IDstripe_subscription_id → Active Stripe Subscriptionsubscription_status → Synced from Stripe webhooksWebhook handling is critical — Stripe sends events (payment succeeded, subscription canceled, trial ending) that must be idempotently processed and immediately reflected in your database.
// Stripe webhook handler (Next.js API route)
export async function POST(req: Request) {
const sig = req.headers.get('stripe-signature')!;
const body = await req.text();
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
switch (event.type) {
case 'customer.subscription.updated':
await db.tenant.update({
where: { stripeCustomerId: event.data.object.customer as string },
data: { subscriptionStatus: event.data.object.status }
});
break;
// Handle other events...
}
return Response.json({ received: true });
}
Within each tenant, you'll have multiple users with different permission levels (Admin, Member, Read-Only, etc.). Implement this as a separate tenant_user_roles table:
CREATE TABLE tenant_user_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id),
user_id UUID REFERENCES users(id),
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(tenant_id, user_id)
);
This architecture is exactly what we used in our Canadian PropTech SaaS case study — delivered in 10 weeks, scaled to 500 customers and $47K MRR.
Ready to build your multi-tenant SaaS? Hire DelhiStack's dedicated SaaS development team.