AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Saas Platforms

skill-miles990-claude-software-skills-saas-platforms · by miles990

SaaS architecture, multi-tenancy, and subscription management

No reviews yet
0 installs
40 views
0.0% view→install

Install

$ agentstack add skill-miles990-claude-software-skills-saas-platforms

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-miles990-claude-software-skills-saas-platforms)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
8mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Saas Platforms? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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(
  tenantId: string,
  fn: () => Promise
): Promise {
  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 = {
    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  {
    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 & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.