| name | saas-platforms |
| description | SaaS architecture, multi-tenancy, and subscription management |
| domain | domain-applications |
| version | 1.0.0 |
| tags | ["saas","multi-tenancy","subscriptions","billing","onboarding"] |
| triggers | {"keywords":{"primary":["saas","multi-tenant","subscription","billing","tenant"],"secondary":["onboarding","feature flag","usage billing","plan","pricing tier"]},"context_boost":["platform","b2b","enterprise","organization"],"context_penalty":["mobile","game","desktop"],"priority":"high"} |
SaaS Platform Development
Overview
Building Software-as-a-Service applications with multi-tenancy, subscription billing, and user management.
Multi-Tenancy
Database Strategies
interface TenantEntity {
tenantId: string;
}
function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
const tenantId = req.headers['x-tenant-id'] || req.user?.tenantId;
if (!tenantId) {
return res.status(400).json({ error: 'Tenant ID required' });
}
req.tenantId = tenantId;
next();
}
prisma.$use(async (params, next) => {
const tenantId = getCurrentTenantId();
if (params.model && hasTenantId(params.model)) {
if (params.action === 'findMany' || params.action === 'findFirst') {
params.args.where = {
...params.args.where,
tenantId,
};
}
if (params.action === 'create') {
params.args.data.tenantId = tenantId;
}
}
return next(params);
});
async function createTenantSchema(tenantId: string) {
await prisma.$executeRaw`CREATE SCHEMA IF NOT EXISTS ${tenantId}`;
await runMigrations(tenantId);
}
function getTenantConnection(tenantId: string) {
return new PrismaClient({
datasources: {
db: {
url: `${process.env.DATABASE_URL}?schema=${tenantId}`,
},
},
});
}
async function createTenantDatabase(tenantId: string) {
const dbName = `tenant_${tenantId}`;
await adminDb.$executeRaw`CREATE DATABASE ${dbName}`;
return new PrismaClient({
datasources: {
db: {
url: `postgresql://user:pass@host:5432/${dbName}`,
},
},
});
}
Tenant Isolation
const prisma = new PrismaClient().$extends({
query: {
$allModels: {
async findMany({ model, operation, args, query }) {
const tenantId = getCurrentTenantId();
args.where = { ...args.where, tenantId };
return query(args);
},
async create({ model, operation, args, query }) {
const tenantId = getCurrentTenantId();
args.data = { ...args.data, tenantId };
return query(args);
},
},
},
});
async function withTenantContext<T>(
tenantId: string,
fn: () => Promise<T>
): Promise<T> {
await prisma.$executeRaw`SET app.tenant_id = ${tenantId}`;
try {
return await fn();
} finally {
await prisma.;
}
}
Subscription Management
Stripe Subscriptions
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
async function createSubscription(
customerId: string,
priceId: string,
trialDays?: number
) {
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
trial_period_days: trialDays,
payment_behavior: 'default_incomplete',
payment_settings: { save_default_payment_method: 'on_subscription' },
expand: ['latest_invoice.payment_intent'],
});
return subscription;
}
async function updateSubscription(subscriptionId: string, newPriceId: string) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
return stripe.subscriptions.(subscriptionId, {
: [
{
: subscription..[].,
: newPriceId,
},
],
: ,
});
}
() {
(immediate) {
stripe..(subscriptionId);
}
stripe..(subscriptionId, {
: ,
});
}
() {
(event.) {
:
: {
subscription = event.. .;
(subscription);
;
}
: {
subscription = event.. .;
(subscription.);
;
}
: {
invoice = event.. .;
(invoice);
;
}
: {
invoice = event.. .;
(invoice);
;
}
}
}
() {
: <, > = {
: ,
: ,
: ,
};
prisma..({
: { : subscription. },
: {
: subscription.,
: subscription.,
: planMapping[subscription..[]..] || ,
: (subscription. * ),
},
});
}
Usage-Based Billing
async function recordUsage(
subscriptionItemId: string,
quantity: number,
timestamp?: number
) {
await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
quantity,
timestamp: timestamp || Math.floor(Date.now() / 1000),
action: 'increment',
});
}
class UsageTracker {
private buffer: Map<string, number> = new Map();
private flushInterval: NodeJS.Timeout;
constructor(private flushIntervalMs = 60000) {
this.flushInterval = setInterval(() => this.flush(), flushIntervalMs);
}
track(orgId: string, metric: string, amount = ) {
key = ;
..(key, (..(key) || ) + amount);
}
() {
entries = .(..());
..();
( [key, amount] entries) {
[orgId, metric] = key.();
prisma..({
: {
: orgId,
metric,
amount,
: (),
},
});
org = prisma..({
: { : orgId },
: { : },
});
(org?.) {
(org., amount);
}
}
}
}
Feature Flags & Entitlements
interface Plan {
id: string;
name: string;
features: {
[key: string]: boolean | number;
};
limits: {
[key: string]: number;
};
}
const plans: Record<string, Plan> = {
free: {
id: 'free',
name: 'Free',
features: {
basicAnalytics: true,
advancedAnalytics: false,
apiAccess: false,
customBranding: false,
},
limits: {
projects: 3,
teamMembers: 1,
storage: 100,
apiCalls: 1000,
},
},
pro: {
id: 'pro',
name: 'Pro',
features: {
basicAnalytics: true,
advancedAnalytics: true,
apiAccess: ,
: ,
},
: {
: ,
: ,
: ,
: ,
},
},
: {
: ,
: ,
: {
: ,
: ,
: ,
: ,
},
: {
: -,
: -,
: -,
: -,
},
},
};
(): {
plan = plans[org.];
plan?.[feature] ?? ;
}
(): {
plan = plans[org.];
limit = plan?.[resource] ?? ;
limit === - || current < limit;
}
() {
(: , : , : ) => {
org = (req.);
(!(org, feature)) {
res.().({
: ,
: ,
: (feature),
});
}
();
};
}
User Onboarding
interface OnboardingStep {
id: string;
title: string;
completed: boolean;
skippable: boolean;
}
async function getOnboardingProgress(userId: string) {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { organization: true },
});
const steps: OnboardingStep[] = [
{
id: 'profile',
title: 'Complete your profile',
completed: !!user.name && !!user.avatar,
skippable: true,
},
{
id: 'invite_team',
title: 'Invite team members',
completed: user.organization.memberCount > 1,
skippable: true,
},
{
id: 'create_project',
title: 'Create your first project',
completed: user.. > ,
: ,
},
{
: ,
: ,
: user.. > ,
: ,
},
];
completedCount = steps.( s.).;
{
steps,
: .((completedCount / steps.) * ),
: steps.( s. || s.),
};
}
Related Skills
- [[system-design]] - SaaS architecture
- [[security-practices]] - Multi-tenant security
- [[database]] - Tenant data isolation