Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    miles990

    saas-platforms

    miles990/saas-platforms
    Business
    1
    3 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    SaaS architecture, multi-tenancy, and subscription management

    SKILL.md

    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.$executeRaw`CREATE 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.$executeRaw`CREATE 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.$executeRaw`SET app.tenant_id = ${tenantId}`;
      try {
        return await fn();
      } finally {
        await prisma.$executeRaw`RESET 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
    Recommended Servers
    AWS Marketplace
    AWS Marketplace
    Vercel
    Vercel
    Repository
    miles990/claude-software-skills
    Files