
Saas Platforms
- 335 installs
- 19 repo stars
- Updated January 20, 2026
- miles990/claude-software-skills
saas-platforms is a Claude Code skill that architects and implements multi-tenant SaaS backends with authentication, billing hooks, and deployment patterns for developers shipping production SaaS products on common stack
About
saas-platforms is an agent skill from miles990/claude-software-skills that guides developers through designing and implementing multi-tenant SaaS backends. The workflow covers tenant isolation models, authentication flows, billing integration hooks, and deployment patterns suited to common SaaS stacks. Developers reach for it when moving from a single-tenant prototype to a production-ready platform that must onboard organizations, enforce per-tenant data boundaries, and connect to subscription billing. The skill emphasizes repeatable patterns for auth middleware, tenant context propagation, and infrastructure layout rather than one-off CRUD endpoints. Outputs include tenant schema designs, auth integration plans, billing webhook stubs, and deployment blueprints ready to implement on your chosen framework.
- Multi-tenant architecture guidance
- Auth and subscription integration patterns
- Deployment and environment conventions
- API design for SaaS products
- Stack-specific platform best practices
Saas Platforms by the numbers
- 335 all-time installs (skills.sh)
- +8 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,246 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/miles990/claude-software-skills --skill saas-platformsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 335 |
|---|---|
| repo stars | ★ 19 |
| Last updated | January 20, 2026 |
| Repository | miles990/claude-software-skills ↗ |
How do you build multi-tenant SaaS backends?
Architect and implement multi-tenant SaaS backends with auth, billing hooks, and deployment patterns on common SaaS stacks.
Who is it for?
Backend developers scaffolding production multi-tenant SaaS with auth, billing, and deployable service boundaries.
Skip if: Skip saas-platforms for single-tenant apps, frontend-only work, or teams that only need a landing page without backend tenancy.
When should I use this skill?
The user asks to design or implement multi-tenant SaaS auth, billing hooks, tenant isolation, or deployment architecture.
What you get
Tenant isolation design, auth integration plan, billing webhook stubs, and deployment blueprint.
- Tenant schema design
- Auth and billing integration plan
Files
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
/**
* SaaS Billing Configuration Template
* Usage: Billing, subscription, and usage tracking types
*/
// ===========================================
// Plan & Pricing Types
// ===========================================
export interface Plan {
id: string;
name: string;
slug: string;
description: string;
// Pricing
pricing: {
monthly: number;
yearly: number;
currency: string;
};
// Stripe IDs
stripe: {
productId: string;
priceIdMonthly: string;
priceIdYearly: string;
};
// Limits
limits: PlanLimits;
// Features
features: PlanFeature[];
// Display
highlighted: boolean;
sortOrder: number;
}
export interface PlanLimits {
users: number; // Max team members
storage: number; // In bytes
apiRequests: number; // Per month
projects?: number; // If applicable
customDomains?: number;
[key: string]: number | undefined;
}
export interface PlanFeature {
key: string;
name: string;
included: boolean;
limit?: number | 'unlimited';
tooltip?: string;
}
// ===========================================
// Subscription Types
// ===========================================
export interface Subscription {
id: string;
tenantId: string;
planId: string;
// Stripe
stripeSubscriptionId: string;
stripeCustomerId: string;
// Status
status: SubscriptionStatus;
billingCycle: 'monthly' | 'yearly';
// Dates
currentPeriodStart: Date;
currentPeriodEnd: Date;
cancelAt?: Date;
canceledAt?: Date;
trialEnd?: Date;
// Metadata
metadata: Record<string, unknown>;
}
export type SubscriptionStatus =
| 'active'
| 'trialing'
| 'past_due'
| 'canceled'
| 'unpaid'
| 'incomplete'
| 'incomplete_expired';
// ===========================================
// Usage & Metering Types
// ===========================================
export interface UsageRecord {
id: string;
tenantId: string;
metric: string; // e.g., "api_requests", "storage", "users"
value: number;
timestamp: Date;
metadata?: Record<string, unknown>;
}
export interface UsageSummary {
tenantId: string;
period: {
start: Date;
end: Date;
};
metrics: {
[key: string]: {
current: number;
limit: number;
percentage: number;
};
};
}
export interface UsageAlert {
metric: string;
threshold: number; // Percentage (0-100)
notified: boolean;
notifiedAt?: Date;
}
// ===========================================
// Invoice Types
// ===========================================
export interface Invoice {
id: string;
tenantId: string;
stripeInvoiceId: string;
// Amounts
subtotal: number;
tax: number;
total: number;
amountPaid: number;
amountDue: number;
currency: string;
// Status
status: InvoiceStatus;
// Dates
periodStart: Date;
periodEnd: Date;
dueDate?: Date;
paidAt?: Date;
// Items
items: InvoiceItem[];
// URLs
invoicePdf?: string;
hostedInvoiceUrl?: string;
}
export type InvoiceStatus =
| 'draft'
| 'open'
| 'paid'
| 'void'
| 'uncollectible';
export interface InvoiceItem {
description: string;
quantity: number;
unitAmount: number;
amount: number;
}
// ===========================================
// Plans Configuration
// ===========================================
export const PLANS: Plan[] = [
{
id: 'free',
name: 'Free',
slug: 'free',
description: 'For individuals and small projects',
pricing: {
monthly: 0,
yearly: 0,
currency: 'usd',
},
stripe: {
productId: '',
priceIdMonthly: '',
priceIdYearly: '',
},
limits: {
users: 1,
storage: 100 * 1024 * 1024, // 100 MB
apiRequests: 1000,
projects: 3,
},
features: [
{ key: 'basic_features', name: 'Basic features', included: true },
{ key: 'community_support', name: 'Community support', included: true },
{ key: 'api_access', name: 'API access', included: false },
{ key: 'custom_domain', name: 'Custom domain', included: false },
],
highlighted: false,
sortOrder: 0,
},
{
id: 'pro',
name: 'Pro',
slug: 'pro',
description: 'For growing teams',
pricing: {
monthly: 2900, // $29/month
yearly: 29000, // $290/year (2 months free)
currency: 'usd',
},
stripe: {
productId: 'prod_xxx',
priceIdMonthly: 'price_monthly_xxx',
priceIdYearly: 'price_yearly_xxx',
},
limits: {
users: 10,
storage: 10 * 1024 * 1024 * 1024, // 10 GB
apiRequests: 100000,
projects: 50,
customDomains: 1,
},
features: [
{ key: 'basic_features', name: 'All Free features', included: true },
{ key: 'api_access', name: 'API access', included: true, limit: 100000 },
{ key: 'custom_domain', name: 'Custom domain', included: true, limit: 1 },
{ key: 'priority_support', name: 'Priority support', included: true },
{ key: 'analytics', name: 'Advanced analytics', included: true },
],
highlighted: true,
sortOrder: 1,
},
{
id: 'enterprise',
name: 'Enterprise',
slug: 'enterprise',
description: 'For large organizations',
pricing: {
monthly: 9900, // $99/month
yearly: 99000, // $990/year
currency: 'usd',
},
stripe: {
productId: 'prod_yyy',
priceIdMonthly: 'price_monthly_yyy',
priceIdYearly: 'price_yearly_yyy',
},
limits: {
users: -1, // Unlimited
storage: 100 * 1024 * 1024 * 1024, // 100 GB
apiRequests: -1, // Unlimited
projects: -1, // Unlimited
customDomains: -1,
},
features: [
{ key: 'basic_features', name: 'All Pro features', included: true },
{ key: 'unlimited_users', name: 'Unlimited users', included: true },
{ key: 'sso', name: 'SSO / SAML', included: true },
{ key: 'audit_logs', name: 'Audit logs', included: true },
{ key: 'sla', name: '99.9% SLA', included: true },
{ key: 'dedicated_support', name: 'Dedicated support', included: true },
],
highlighted: false,
sortOrder: 2,
},
];
// ===========================================
// Billing Utilities
// ===========================================
export function getPlanById(id: string): Plan | undefined {
return PLANS.find(p => p.id === id);
}
export function getPlanBySlug(slug: string): Plan | undefined {
return PLANS.find(p => p.slug === slug);
}
export function formatPrice(amount: number, currency: string = 'usd'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase(),
}).format(amount / 100);
}
export function isWithinLimit(
current: number,
limit: number
): boolean {
return limit === -1 || current < limit;
}
export function getUsagePercentage(
current: number,
limit: number
): number {
if (limit === -1) return 0;
return Math.min(100, Math.round((current / limit) * 100));
}
export function checkLimitExceeded(
usage: UsageSummary,
metric: string
): boolean {
const m = usage.metrics[metric];
if (!m) return false;
return m.percentage >= 100;
}
// ===========================================
// Webhook Event Types (Stripe)
// ===========================================
export type BillingEvent =
| 'subscription.created'
| 'subscription.updated'
| 'subscription.deleted'
| 'invoice.paid'
| 'invoice.payment_failed'
| 'customer.subscription.trial_will_end';
export interface BillingWebhookPayload {
event: BillingEvent;
data: {
tenantId: string;
subscriptionId?: string;
invoiceId?: string;
amount?: number;
[key: string]: unknown;
};
}
SaaS Platform Templates
Multi-tenant architecture and billing templates for SaaS applications.
Files
| Template | Purpose |
|---|---|
tenant-schema.prisma | Multi-tenant database schema |
billing-config.ts | Plans, subscriptions, and billing types |
Usage
Multi-tenant Schema
# Initialize Prisma
npx prisma init
# Copy schema
cp tenant-schema.prisma prisma/schema.prisma
# Generate client
npx prisma generate
# Run migrations
npx prisma migrate devBilling Configuration
import {
PLANS,
getPlanById,
formatPrice,
isWithinLimit,
getUsagePercentage,
} from './billing-config';
// Get plan
const proPlan = getPlanById('pro');
// Format price
const price = formatPrice(2900, 'usd'); // "$29.00"
// Check limits
const canAddUser = isWithinLimit(currentUsers, plan.limits.users);
// Get usage percentage
const storageUsage = getUsagePercentage(usedStorage, plan.limits.storage);Data Models
Tenant Hierarchy
Tenant
├── Users (via Membership)
│ ├── Owner
│ ├── Admin
│ ├── Member
│ └── Viewer
├── Plan
├── API Keys
├── Invitations
└── SettingsSubscription Lifecycle
trialing → active → past_due → canceled
↘ unpaidKey Patterns
Row-Level Security
// Middleware to filter by tenant
app.use((req, res, next) => {
const tenantId = req.user?.tenantId;
if (tenantId) {
prisma.$use(async (params, next) => {
// Add tenantId filter to all queries
if (params.args?.where) {
params.args.where.tenantId = tenantId;
}
return next(params);
});
}
next();
});Usage Tracking
async function trackUsage(
tenantId: string,
metric: string,
value: number
) {
await prisma.usageRecord.create({
data: { tenantId, metric, value, timestamp: new Date() }
});
// Check limit
const summary = await getUsageSummary(tenantId);
if (summary.metrics[metric].percentage >= 80) {
await sendUsageAlert(tenantId, metric);
}
}Plan Upgrade/Downgrade
async function changePlan(
tenantId: string,
newPlanId: string,
prorate: boolean = true
) {
const subscription = await getSubscription(tenantId);
await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
items: [{
id: subscription.itemId,
price: newPlan.stripe.priceIdMonthly,
}],
proration_behavior: prorate ? 'create_prorations' : 'none',
});
}Stripe Integration
Webhook Handler
app.post('/webhooks/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
switch (event.type) {
case 'invoice.paid':
await handleInvoicePaid(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object);
break;
}
res.json({ received: true });
});Customer Portal
const session = await stripe.billingPortal.sessions.create({
customer: tenant.stripeCustomerId,
return_url: `${BASE_URL}/settings/billing`,
});
// Redirect to session.urlEnvironment Variables
# Database
DATABASE_URL="postgresql://..."
# Stripe
STRIPE_SECRET_KEY=sk_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PUBLISHABLE_KEY=pk_...// ===========================================
// SaaS Multi-tenant Schema Template
// Usage: Prisma schema for multi-tenant SaaS
// ===========================================
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ===========================================
// Tenant & Organization
// ===========================================
model Tenant {
id String @id @default(cuid())
name String
slug String @unique
domain String? @unique // Custom domain
// Plan & Billing
planId String?
plan Plan? @relation(fields: [planId], references: [id])
billingEmail String?
stripeCustomerId String? @unique
// Settings
settings Json @default("{}")
features String[] @default([])
// Limits
maxUsers Int @default(5)
maxStorage BigInt @default(1073741824) // 1GB in bytes
// Status
status TenantStatus @default(ACTIVE)
trialEndsAt DateTime?
// Relations
users User[]
memberships Membership[]
invitations Invitation[]
apiKeys ApiKey[]
// Audit
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([slug])
@@index([status])
}
enum TenantStatus {
ACTIVE
TRIAL
SUSPENDED
CANCELLED
}
// ===========================================
// Users & Authentication
// ===========================================
model User {
id String @id @default(cuid())
email String @unique
emailVerified DateTime?
name String?
image String?
// Auth
passwordHash String?
// Multi-tenant
tenantId String?
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
memberships Membership[]
// Sessions
sessions Session[]
// Audit
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@index([tenantId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expires DateTime
// Device info
userAgent String?
ipAddress String?
createdAt DateTime @default(now())
@@index([userId])
}
// ===========================================
// Team & Permissions
// ===========================================
model Membership {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
role Role @default(MEMBER)
// Audit
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, tenantId])
@@index([tenantId])
}
enum Role {
OWNER
ADMIN
MEMBER
VIEWER
}
model Invitation {
id String @id @default(cuid())
email String
role Role @default(MEMBER)
token String @unique @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
invitedBy String // User ID who sent invitation
expiresAt DateTime
acceptedAt DateTime?
createdAt DateTime @default(now())
@@unique([email, tenantId])
@@index([token])
@@index([tenantId])
}
// ===========================================
// Plans & Subscriptions
// ===========================================
model Plan {
id String @id @default(cuid())
name String
slug String @unique
description String?
// Pricing
priceMonthly Int // In cents
priceYearly Int // In cents
currency String @default("usd")
// Stripe
stripePriceIdMonthly String?
stripePriceIdYearly String?
// Limits
maxUsers Int
maxStorage BigInt // In bytes
features String[] @default([])
// Status
isPublic Boolean @default(true)
sortOrder Int @default(0)
// Relations
tenants Tenant[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([slug])
}
// ===========================================
// API Keys
// ===========================================
model ApiKey {
id String @id @default(cuid())
name String
keyHash String @unique // Hashed API key
keyPrefix String // First 8 chars for identification
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
// Permissions
scopes String[] @default(["read"])
// Usage
lastUsedAt DateTime?
usageCount Int @default(0)
// Expiry
expiresAt DateTime?
revokedAt DateTime?
createdAt DateTime @default(now())
@@index([tenantId])
@@index([keyPrefix])
}
// ===========================================
// Audit Log
// ===========================================
model AuditLog {
id String @id @default(cuid())
tenantId String
userId String?
action String // e.g., "user.created", "settings.updated"
resource String // e.g., "user", "tenant"
resourceId String?
metadata Json @default("{}")
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@index([tenantId])
@@index([userId])
@@index([action])
@@index([createdAt])
}
// ===========================================
// Feature Flags
// ===========================================
model FeatureFlag {
id String @id @default(cuid())
key String @unique
name String
description String?
// Targeting
enabled Boolean @default(false)
percentage Int @default(100) // Rollout percentage
tenantIds String[] @default([]) // Specific tenants
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([key])
}
Related skills
FAQ
What does saas-platforms help developers build?
saas-platforms architects multi-tenant SaaS backends with authentication, billing hooks, and deployment patterns. It produces tenant isolation designs and integration plans for production SaaS APIs.
Is saas-platforms frontend or backend focused?
saas-platforms focuses on backend tenancy, auth, billing integration, and deployment architecture. It does not cover marketing pages or UI component libraries.