Multi-tenancy: the central architectural decision of every SaaS
Multi-tenancy is how your system serves multiple customers (tenants) from the same infrastructure. The three main models have very different tradeoffs in cost, security, and operational complexity.
Silo Model: dedicated infrastructure per tenant
Each tenant has their own application instance and database. Isolation is total: a bug or breach in tenant A doesn't affect tenant B. Operational cost is high — each new tenant requires provisioning infrastructure. The right model for enterprise customers with compliance requirements (data in their own VPC, full database control).
Pool Model: shared database with tenant_id
All tenants share the same database. Isolation is managed at the application level with a tenant_id in every table. Operational cost is low — onboarding a new tenant is inserting a record. The risk: a tenancy leak bug (a query that forgets the tenant_id filter) can expose one tenant's data to another.
Bridge Model: schema per tenant in the same database instance
Each tenant has their own schema within the same PostgreSQL instance. Better isolation than Pool without the cost of separate instances. The complexity: managing schema migrations across all active tenants.
// Multi-tenancy middleware with tenant_id in Pool Model
import { AsyncLocalStorage } from 'node:async_hooks';
const tenantStorage = new AsyncLocalStorage<string>();
export const tenantMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const tenantId = req.auth?.tenantId;
if (!tenantId) return res.status(401).json({ error: 'Tenant not identified' });
tenantStorage.run(tenantId, () => next());
};
// Prisma extension: automatically add tenant_id to all queries
export const tenantPrismaExtension = Prisma.defineExtension({
query: {
$allModels: {
async findMany({ args, query }) {
const tenantId = tenantStorage.getStore();
if (!tenantId) throw new Error('Tenant context not set');
args.where = { ...args.where, tenant_id: tenantId };
return query(args);
},
async create({ args, query }) {
const tenantId = tenantStorage.getStore();
if (!tenantId) throw new Error('Tenant context not set');
args.data = { ...args.data, tenant_id: tenantId };
return query(args);
},
},
},
});Usage-based billing: the SaaS financial engine
- Flat rate: fixed monthly price. Simple to implement, predictable for the customer, but can penalize low-usage customers while subsidizing high-usage ones.
- Per seat: price per active user. The most common in B2B SaaS. Natural incentive for customers to only keep the users they need.
- Usage-based (consumption): price per unit of use (API calls, GB stored, transactions processed). Lower entry barrier, but unpredictable revenue. Growing trend in infrastructure and AI SaaS.
// Usage recording for consumption billing with Stripe Metered Billing
async function recordApiUsage(tenantId: string, count: number): Promise<void> {
const subscription = await db.subscription.findUnique({
where: { tenantId },
select: { stripeSubscriptionItemId: true }
});
if (!subscription?.stripeSubscriptionItemId) return;
await stripe.subscriptionItems.createUsageRecord(
subscription.stripeSubscriptionItemId,
{
quantity: count,
timestamp: Math.floor(Date.now() / 1000),
action: 'increment',
}
);
}Automated onboarding: the flow that determines retention
In B2B SaaS, time-to-value is the metric most correlated with retention. A customer who reaches first concrete value in under 24 hours is 3x more likely to convert from trial to paid than one who takes a week. Technical onboarding must be fully automated.
Feature flags and plans: SaaS access control
const PLAN_FEATURES = {
starter: ['api_access', 'basic_reports', 'email_support'],
professional: ['api_access', 'advanced_reports', 'webhooks', 'priority_support', 'sso'],
enterprise: ['api_access', 'advanced_reports', 'webhooks', 'priority_support', 'sso',
'custom_domain', 'audit_logs', 'dedicated_support', 'sla_guarantee'],
} as const;
async function hasFeature(tenantId: string, feature: string): Promise<boolean> {
const tenant = await getTenantWithPlan(tenantId);
const planFeatures = PLAN_FEATURES[tenant.plan as keyof typeof PLAN_FEATURES] ?? [];
return (planFeatures as string[]).includes(feature);
}
if (!await hasFeature(tenantId, 'webhooks')) {
return res.status(402).json({
error: 'This feature requires the Professional plan or higher',
upgradeUrl: '/settings/billing'
});
}SaaS observability: business metrics as technical metrics
- Alert when the number of active tenants in the last 24h drops 20% vs. weekly average (signal of possible login or critical feature issue)
- Track feature adoption by plan (what % of Professional tenants use SSO) to inform roadmap decisions
- Monitor average onboarding completion time by signup cohort
Frequently Asked Questions
Which multi-tenancy model should I choose for a new B2B SaaS?
How do I handle data security between tenants in the Pool Model?
Stripe or a custom billing system for a SaaS?
How do I design architecture to support SSO/SAML that enterprise customers require?
How do I manage database migrations in a SaaS with many tenants in Bridge Model?
Are you building a SaaS product and want to make the right architecture decisions from the start? Our team can guide you through the design.
Talk to our team