saas-platforms

安装量: 98
排名: #8445

安装

npx skills add https://github.com/miles990/claude-software-skills --skill 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( tenantId: string, fn: () => Promise ): Promise { 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 = { 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 = 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 = { 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

返回排行榜