Claude Code Plugins

Community-maintained marketplace

Feedback

SaaS architecture, multi-tenancy, and subscription management

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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 [object Object]

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