saas-platforms

SaaS Platform Development

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "saas-platforms" with this command: npx skills add miles990/claude-software-skills/miles990-claude-software-skills-saas-platforms

SaaS Platform Development

Overview

Building Software-as-a-Service applications with multi-tenancy, subscription billing, and user management.

Multi-Tenancy

Database Strategies

// Strategy 1: Shared database with tenant_id column interface TenantEntity { tenantId: string; // ... other fields }

// Middleware to inject tenant context 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 middleware for automatic tenant filtering prisma.$use(async (params, next) => { const tenantId = getCurrentTenantId();

if (params.model && hasTenantId(params.model)) { // Add tenant filter to queries if (params.action === 'findMany' || params.action === 'findFirst') { params.args.where = { ...params.args.where, tenantId, }; }

// Add tenant ID to creates
if (params.action === 'create') {
  params.args.data.tenantId = tenantId;
}

}

return next(params); });

// Strategy 2: Schema per tenant (PostgreSQL) async function createTenantSchema(tenantId: string) { await prisma.$executeRawCREATE SCHEMA IF NOT EXISTS ${tenantId};

// Run migrations for new schema await runMigrations(tenantId); }

function getTenantConnection(tenantId: string) { return new PrismaClient({ datasources: { db: { url: ${process.env.DATABASE_URL}?schema=${tenantId}, }, }, }); }

// Strategy 3: Database per tenant async function createTenantDatabase(tenantId: string) { const dbName = tenant_${tenantId}; await adminDb.$executeRawCREATE DATABASE ${dbName};

return new PrismaClient({ datasources: { db: { url: postgresql://user:pass@host:5432/${dbName}, }, }, }); }

Tenant Isolation

// Row-level security with Prisma 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); }, }, }, });

// PostgreSQL Row Level Security /* CREATE POLICY tenant_isolation ON projects USING (tenant_id = current_setting('app.tenant_id')::uuid);

ALTER TABLE projects ENABLE ROW LEVEL SECURITY; */

// Set tenant context for RLS async function withTenantContext<T>( tenantId: string, fn: () => Promise<T> ): Promise<T> { await prisma.$executeRawSET app.tenant_id = ${tenantId}; try { return await fn(); } finally { await prisma.$executeRawRESET app.tenant_id; } }

Subscription Management

Stripe Subscriptions

import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// Create subscription 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; }

// Update subscription async function updateSubscription(subscriptionId: string, newPriceId: string) { const subscription = await stripe.subscriptions.retrieve(subscriptionId);

return stripe.subscriptions.update(subscriptionId, { items: [ { id: subscription.items.data[0].id, price: newPriceId, }, ], proration_behavior: 'create_prorations', }); }

// Cancel subscription async function cancelSubscription(subscriptionId: string, immediate = false) { if (immediate) { return stripe.subscriptions.cancel(subscriptionId); }

return stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: true, }); }

// Handle subscription webhooks async function handleSubscriptionWebhook(event: Stripe.Event) { switch (event.type) { case 'customer.subscription.created': case 'customer.subscription.updated': { const subscription = event.data.object as Stripe.Subscription; await syncSubscription(subscription); break; }

case 'customer.subscription.deleted': {
  const subscription = event.data.object as Stripe.Subscription;
  await deactivateSubscription(subscription.id);
  break;
}

case 'invoice.payment_succeeded': {
  const invoice = event.data.object as Stripe.Invoice;
  await recordPayment(invoice);
  break;
}

case 'invoice.payment_failed': {
  const invoice = event.data.object as Stripe.Invoice;
  await handleFailedPayment(invoice);
  break;
}

} }

// Sync subscription to database async function syncSubscription(subscription: Stripe.Subscription) { const planMapping: Record<string, string> = { price_starter: 'starter', price_pro: 'pro', price_enterprise: 'enterprise', };

await prisma.organization.update({ where: { stripeCustomerId: subscription.customer as string }, data: { subscriptionId: subscription.id, subscriptionStatus: subscription.status, plan: planMapping[subscription.items.data[0].price.id] || 'free', currentPeriodEnd: new Date(subscription.current_period_end * 1000), }, }); }

Usage-Based Billing

// Track usage async function recordUsage( subscriptionItemId: string, quantity: number, timestamp?: number ) { await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, { quantity, timestamp: timestamp || Math.floor(Date.now() / 1000), action: 'increment', }); }

// Usage tracking service 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 = 1) { const key = ${orgId}:${metric}; this.buffer.set(key, (this.buffer.get(key) || 0) + amount); }

async flush() { const entries = Array.from(this.buffer.entries()); this.buffer.clear();

for (const [key, amount] of entries) {
  const [orgId, metric] = key.split(':');

  // Record to database
  await prisma.usageRecord.create({
    data: {
      organizationId: orgId,
      metric,
      amount,
      timestamp: new Date(),
    },
  });

  // Report to Stripe (for metered billing)
  const org = await prisma.organization.findUnique({
    where: { id: orgId },
    select: { subscriptionItemId: true },
  });

  if (org?.subscriptionItemId) {
    await recordUsage(org.subscriptionItemId, 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, // MB apiCalls: 1000, }, }, pro: { id: 'pro', name: 'Pro', features: { basicAnalytics: true, advancedAnalytics: true, apiAccess: true, customBranding: false, }, limits: { projects: 20, teamMembers: 10, storage: 10000, // MB apiCalls: 100000, }, }, enterprise: { id: 'enterprise', name: 'Enterprise', features: { basicAnalytics: true, advancedAnalytics: true, apiAccess: true, customBranding: true, }, limits: { projects: -1, // Unlimited teamMembers: -1, storage: -1, apiCalls: -1, }, }, };

// Check feature access function hasFeature(org: Organization, feature: string): boolean { const plan = plans[org.plan]; return plan?.features[feature] ?? false; }

// Check limit function checkLimit(org: Organization, resource: string, current: number): boolean { const plan = plans[org.plan]; const limit = plan?.limits[resource] ?? 0; return limit === -1 || current < limit; }

// Middleware for feature gating function requireFeature(feature: string) { return async (req: Request, res: Response, next: NextFunction) => { const org = await getOrganization(req.tenantId);

if (!hasFeature(org, feature)) {
  return res.status(403).json({
    error: 'Feature not available',
    upgrade: true,
    requiredPlan: getMinimumPlanForFeature(feature),
  });
}

next();

}; }

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.organization.projectCount > 0, skippable: false, }, { id: 'connect_integration', title: 'Connect an integration', completed: user.organization.integrationCount > 0, skippable: true, }, ];

const completedCount = steps.filter((s) => s.completed).length;

return { steps, progress: Math.round((completedCount / steps.length) * 100), isComplete: steps.every((s) => s.completed || s.skippable), }; }

Related Skills

  • [[system-design]] - SaaS architecture

  • [[security-practices]] - Multi-tenant security

  • [[database]] - Tenant data isolation

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

code-quality

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

game-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

devops-cicd

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python

No summary provided by upstream source.

Repository SourceNeeds Review