saas-scaffolder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SaaS Scaffolder

SaaS 脚手架生成器

Tier: POWERFUL Category: Engineering / Full-Stack Maintainer: Claude Skills Team
等级: 强大 分类: 工程 / 全栈 维护者: Claude Skills Team

Overview

概述

Generate a complete, production-ready SaaS application boilerplate including authentication (NextAuth, Clerk, or Supabase Auth), database schemas with multi-tenancy, billing integration (Stripe or Lemon Squeezy), API routes with validation, dashboard UI with shadcn/ui, and deployment configuration. Produces a working application from a product specification in under 30 minutes.
生成可直接用于生产环境的完整SaaS应用模板,包含身份认证(NextAuth、Clerk或Supabase Auth)、支持多租户的数据库schema、支付集成(Stripe或Lemon Squeezy)、带校验的API路由、基于shadcn/ui的仪表盘UI以及部署配置。只需30分钟即可根据产品规格生成可运行的应用。

Keywords

关键词

SaaS, boilerplate, scaffolding, Next.js, authentication, Stripe, billing, multi-tenancy, subscription, starter template, NextAuth, Drizzle ORM, shadcn/ui
SaaS、项目模板、脚手架、Next.js、身份认证、Stripe、支付计费、多租户、订阅、启动模板、NextAuth、Drizzle ORM、shadcn/ui

Input Specification

输入规格

Product: [name]
Description: [1-3 sentences]
Auth: nextauth | clerk | supabase
Database: neondb | supabase | planetscale | turso
Payments: stripe | lemonsqueezy | none
Multi-tenancy: workspace | organization | none
Features: [comma-separated list]
Product: [名称]
Description: [1-3句话]
Auth: nextauth | clerk | supabase
Database: neondb | supabase | planetscale | turso
Payments: stripe | lemonsqueezy | none
Multi-tenancy: workspace | organization | none
Features: [逗号分隔的列表]

Generated File Tree

生成的文件目录

my-saas/
├── app/
│   ├── (auth)/
│   │   ├── login/page.tsx
│   │   ├── register/page.tsx
│   │   ├── forgot-password/page.tsx
│   │   └── layout.tsx
│   ├── (dashboard)/
│   │   ├── dashboard/page.tsx
│   │   ├── settings/
│   │   │   ├── page.tsx              # Profile settings
│   │   │   ├── billing/page.tsx      # Subscription management
│   │   │   └── team/page.tsx         # Team/workspace settings
│   │   └── layout.tsx                # Dashboard shell (sidebar + header)
│   ├── (marketing)/
│   │   ├── page.tsx                  # Landing page
│   │   ├── pricing/page.tsx          # Pricing tiers
│   │   └── layout.tsx
│   ├── api/
│   │   ├── auth/[...nextauth]/route.ts
│   │   ├── webhooks/stripe/route.ts
│   │   ├── billing/
│   │   │   ├── checkout/route.ts
│   │   │   └── portal/route.ts
│   │   └── health/route.ts
│   ├── layout.tsx                    # Root layout
│   └── not-found.tsx
├── components/
│   ├── ui/                           # shadcn/ui components
│   ├── auth/
│   │   ├── login-form.tsx
│   │   └── register-form.tsx
│   ├── dashboard/
│   │   ├── sidebar.tsx
│   │   ├── header.tsx
│   │   └── stats-card.tsx
│   ├── marketing/
│   │   ├── hero.tsx
│   │   ├── features.tsx
│   │   ├── pricing-card.tsx
│   │   └── footer.tsx
│   └── billing/
│       ├── plan-card.tsx
│       └── usage-meter.tsx
├── lib/
│   ├── auth.ts                       # Auth configuration
│   ├── db.ts                         # Database client singleton
│   ├── stripe.ts                     # Stripe client
│   ├── validations.ts                # Zod schemas
│   └── utils.ts                      # Shared utilities
├── db/
│   ├── schema.ts                     # Drizzle schema
│   ├── migrations/                   # Generated migrations
│   └── seed.ts                       # Development seed data
├── hooks/
│   ├── use-subscription.ts
│   └── use-current-user.ts
├── types/
│   └── index.ts                      # Shared TypeScript types
├── middleware.ts                      # Auth + rate limiting
├── .env.example
├── drizzle.config.ts
├── tailwind.config.ts
└── next.config.ts
my-saas/
├── app/
│   ├── (auth)/
│   │   ├── login/page.tsx
│   │   ├── register/page.tsx
│   │   ├── forgot-password/page.tsx
│   │   └── layout.tsx
│   ├── (dashboard)/
│   │   ├── dashboard/page.tsx
│   │   ├── settings/
│   │   │   ├── page.tsx              # 个人设置
│   │   │   ├── billing/page.tsx      # 订阅管理
│   │   │   └── team/page.tsx         # 团队/工作区设置
│   │   └── layout.tsx                # 仪表盘框架(侧边栏 + 头部)
│   ├── (marketing)/
│   │   ├── page.tsx                  # 着陆页
│   │   ├── pricing/page.tsx          # 定价层级
│   │   └── layout.tsx
│   ├── api/
│   │   ├── auth/[...nextauth]/route.ts
│   │   ├── webhooks/stripe/route.ts
│   │   ├── billing/
│   │   │   ├── checkout/route.ts
│   │   │   └── portal/route.ts
│   │   └── health/route.ts
│   ├── layout.tsx                    # 根布局
│   └── not-found.tsx
├── components/
│   ├── ui/                           # shadcn/ui 组件
│   ├── auth/
│   │   ├── login-form.tsx
│   │   └── register-form.tsx
│   ├── dashboard/
│   │   ├── sidebar.tsx
│   │   ├── header.tsx
│   │   └── stats-card.tsx
│   ├── marketing/
│   │   ├── hero.tsx
│   │   ├── features.tsx
│   │   ├── pricing-card.tsx
│   │   └── footer.tsx
│   └── billing/
│       ├── plan-card.tsx
│       └── usage-meter.tsx
├── lib/
│   ├── auth.ts                       # 身份认证配置
│   ├── db.ts                         # 数据库客户端单例
│   ├── stripe.ts                     # Stripe 客户端
│   ├── validations.ts                # Zod 校验schema
│   └── utils.ts                      # 通用工具函数
├── db/
│   ├── schema.ts                     # Drizzle schema
│   ├── migrations/                   # 生成的迁移文件
│   └── seed.ts                       # 开发环境测试数据
├── hooks/
│   ├── use-subscription.ts
│   └── use-current-user.ts
├── types/
│   └── index.ts                      # 共享TypeScript类型
├── middleware.ts                      # 身份认证 + 限流
├── .env.example
├── drizzle.config.ts
├── tailwind.config.ts
└── next.config.ts

Database Schema (Multi-Tenant)

数据库Schema(多租户)

typescript
// db/schema.ts
import { pgTable, text, timestamp, integer, boolean, uniqueIndex, index } from 'drizzle-orm/pg-core'
import { createId } from '@paralleldrive/cuid2'

// ──── WORKSPACES (Tenancy boundary) ────
export const workspaces = pgTable('workspaces', {
  id: text('id').primaryKey().$defaultFn(createId),
  name: text('name').notNull(),
  slug: text('slug').notNull(),
  plan: text('plan').notNull().default('free'),  // free | pro | enterprise
  stripeCustomerId: text('stripe_customer_id').unique(),
  stripeSubscriptionId: text('stripe_subscription_id'),
  stripePriceId: text('stripe_price_id'),
  stripeCurrentPeriodEnd: timestamp('stripe_current_period_end'),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (t) => [
  uniqueIndex('workspaces_slug_idx').on(t.slug),
])

// ──── USERS ────
export const users = pgTable('users', {
  id: text('id').primaryKey().$defaultFn(createId),
  email: text('email').notNull().unique(),
  name: text('name'),
  avatarUrl: text('avatar_url'),
  emailVerified: timestamp('email_verified', { withTimezone: true }),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
})

// ──── WORKSPACE MEMBERS ────
export const workspaceMembers = pgTable('workspace_members', {
  id: text('id').primaryKey().$defaultFn(createId),
  workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }),
  userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  role: text('role').notNull().default('member'), // owner | admin | member
  joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull(),
}, (t) => [
  uniqueIndex('workspace_members_unique').on(t.workspaceId, t.userId),
  index('workspace_members_workspace_idx').on(t.workspaceId),
])

// ──── ACCOUNTS (OAuth) ────
export const accounts = pgTable('accounts', {
  userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  type: text('type').notNull(),
  provider: text('provider').notNull(),
  providerAccountId: text('provider_account_id').notNull(),
  refreshToken: text('refresh_token'),
  accessToken: text('access_token'),
  expiresAt: integer('expires_at'),
})

// ──── SESSIONS ────
export const sessions = pgTable('sessions', {
  sessionToken: text('session_token').primaryKey(),
  userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  expires: timestamp('expires', { withTimezone: true }).notNull(),
})
typescript
// db/schema.ts
import { pgTable, text, timestamp, integer, boolean, uniqueIndex, index } from 'drizzle-orm/pg-core'
import { createId } from '@paralleldrive/cuid2'

// ──── 工作区(租户边界) ────
export const workspaces = pgTable('workspaces', {
  id: text('id').primaryKey().$defaultFn(createId),
  name: text('name').notNull(),
  slug: text('slug').notNull(),
  plan: text('plan').notNull().default('free'),  // free | pro | enterprise
  stripeCustomerId: text('stripe_customer_id').unique(),
  stripeSubscriptionId: text('stripe_subscription_id'),
  stripePriceId: text('stripe_price_id'),
  stripeCurrentPeriodEnd: timestamp('stripe_current_period_end'),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (t) => [
  uniqueIndex('workspaces_slug_idx').on(t.slug),
])

// ──── 用户 ────
export const users = pgTable('users', {
  id: text('id').primaryKey().$defaultFn(createId),
  email: text('email').notNull().unique(),
  name: text('name'),
  avatarUrl: text('avatar_url'),
  emailVerified: timestamp('email_verified', { withTimezone: true }),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
})

// ──── 工作区成员 ────
export const workspaceMembers = pgTable('workspace_members', {
  id: text('id').primaryKey().$defaultFn(createId),
  workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }),
  userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  role: text('role').notNull().default('member'), // owner | admin | member
  joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull(),
}, (t) => [
  uniqueIndex('workspace_members_unique').on(t.workspaceId, t.userId),
  index('workspace_members_workspace_idx').on(t.workspaceId),
])

// ──── 账号(OAuth) ────
export const accounts = pgTable('accounts', {
  userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  type: text('type').notNull(),
  provider: text('provider').notNull(),
  providerAccountId: text('provider_account_id').notNull(),
  refreshToken: text('refresh_token'),
  accessToken: text('access_token'),
  expiresAt: integer('expires_at'),
})

// ──── 会话 ────
export const sessions = pgTable('sessions', {
  sessionToken: text('session_token').primaryKey(),
  userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  expires: timestamp('expires', { withTimezone: true }).notNull(),
})

Authentication Configuration

身份认证配置

typescript
// lib/auth.ts
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
import GitHub from 'next-auth/providers/github'
import Resend from 'next-auth/providers/resend'
import { db } from './db'

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    Resend({
      from: 'noreply@myapp.com',
    }),
  ],
  callbacks: {
    session: async ({ session, user }) => ({
      ...session,
      user: {
        ...session.user,
        id: user.id,
      },
    }),
  },
  pages: {
    signIn: '/login',
    error: '/login',
  },
})
typescript
// lib/auth.ts
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
import GitHub from 'next-auth/providers/github'
import Resend from 'next-auth/providers/resend'
import { db } from './db'

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    Resend({
      from: 'noreply@myapp.com',
    }),
  ],
  callbacks: {
    session: async ({ session, user }) => ({
      ...session,
      user: {
        ...session.user,
        id: user.id,
      },
    }),
  },
  pages: {
    signIn: '/login',
    error: '/login',
  },
})

Stripe Billing Integration

Stripe支付集成

Checkout Session

结账会话

typescript
// app/api/billing/checkout/route.ts
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { stripe } from '@/lib/stripe'
import { db } from '@/lib/db'
import { workspaces } from '@/db/schema'
import { eq } from 'drizzle-orm'

export async function POST(req: Request) {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { priceId, workspaceId } = await req.json()

  // Get or create Stripe customer
  const [workspace] = await db.select().from(workspaces).where(eq(workspaces.id, workspaceId))
  if (!workspace) {
    return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
  }

  let customerId = workspace.stripeCustomerId
  if (!customerId) {
    const customer = await stripe.customers.create({
      email: session.user.email!,
      metadata: { workspaceId },
    })
    customerId = customer.id
    await db.update(workspaces)
      .set({ stripeCustomerId: customerId })
      .where(eq(workspaces.id, workspaceId))
  }

  const checkoutSession = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    subscription_data: { trial_period_days: 14 },
    metadata: { workspaceId },
  })

  return NextResponse.json({ url: checkoutSession.url })
}
typescript
// app/api/billing/checkout/route.ts
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { stripe } from '@/lib/stripe'
import { db } from '@/lib/db'
import { workspaces } from '@/db/schema'
import { eq } from 'drizzle-orm'

export async function POST(req: Request) {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { priceId, workspaceId } = await req.json()

  // 获取或创建Stripe客户
  const [workspace] = await db.select().from(workspaces).where(eq(workspaces.id, workspaceId))
  if (!workspace) {
    return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
  }

  let customerId = workspace.stripeCustomerId
  if (!customerId) {
    const customer = await stripe.customers.create({
      email: session.user.email!,
      metadata: { workspaceId },
    })
    customerId = customer.id
    await db.update(workspaces)
      .set({ stripeCustomerId: customerId })
      .where(eq(workspaces.id, workspaceId))
  }

  const checkoutSession = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    subscription_data: { trial_period_days: 14 },
    metadata: { workspaceId },
  })

  return NextResponse.json({ url: checkoutSession.url })
}

Webhook Handler

Webhook处理器

typescript
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers'
import { stripe } from '@/lib/stripe'
import { db } from '@/lib/db'
import { workspaces } from '@/db/schema'
import { eq } from 'drizzle-orm'

export async function POST(req: Request) {
  const body = await req.text()
  const signature = (await headers()).get('Stripe-Signature')!

  let event
  try {
    event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)
  } catch (err) {
    return new Response(`Webhook Error: ${err.message}`, { status: 400 })
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object
      const subscription = await stripe.subscriptions.retrieve(session.subscription as string)
      await db.update(workspaces).set({
        stripeSubscriptionId: subscription.id,
        stripePriceId: subscription.items.data[0].price.id,
        stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
      }).where(eq(workspaces.stripeCustomerId, session.customer as string))
      break
    }

    case 'invoice.payment_succeeded': {
      const invoice = event.data.object
      const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)
      await db.update(workspaces).set({
        stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
      }).where(eq(workspaces.stripeCustomerId, invoice.customer as string))
      break
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object
      await db.update(workspaces).set({
        plan: 'free',
        stripeSubscriptionId: null,
        stripePriceId: null,
        stripeCurrentPeriodEnd: null,
      }).where(eq(workspaces.stripeCustomerId, subscription.customer as string))
      break
    }
  }

  return new Response('OK', { status: 200 })
}
typescript
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers'
import { stripe } from '@/lib/stripe'
import { db } from '@/lib/db'
import { workspaces } from '@/db/schema'
import { eq } from 'drizzle-orm'

export async function POST(req: Request) {
  const body = await req.text()
  const signature = (await headers()).get('Stripe-Signature')!

  let event
  try {
    event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)
  } catch (err) {
    return new Response(`Webhook Error: ${err.message}`, { status: 400 })
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object
      const subscription = await stripe.subscriptions.retrieve(session.subscription as string)
      await db.update(workspaces).set({
        stripeSubscriptionId: subscription.id,
        stripePriceId: subscription.items.data[0].price.id,
        stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
      }).where(eq(workspaces.stripeCustomerId, session.customer as string))
      break
    }

    case 'invoice.payment_succeeded': {
      const invoice = event.data.object
      const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)
      await db.update(workspaces).set({
        stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
      }).where(eq(workspaces.stripeCustomerId, invoice.customer as string))
      break
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object
      await db.update(workspaces).set({
        plan: 'free',
        stripeSubscriptionId: null,
        stripePriceId: null,
        stripeCurrentPeriodEnd: null,
      }).where(eq(workspaces.stripeCustomerId, subscription.customer as string))
      break
    }
  }

  return new Response('OK', { status: 200 })
}

Middleware (Auth + Rate Limiting)

中间件(身份认证 + 限流)

typescript
// middleware.ts
import { auth } from '@/lib/auth'
import { NextResponse } from 'next/server'

export default auth((req) => {
  const { pathname } = req.nextUrl
  const isAuthenticated = !!req.auth

  // Protected routes
  if (pathname.startsWith('/dashboard') || pathname.startsWith('/settings')) {
    if (!isAuthenticated) {
      return NextResponse.redirect(new URL('/login', req.url))
    }
  }

  // Redirect logged-in users away from auth pages
  if ((pathname === '/login' || pathname === '/register') && isAuthenticated) {
    return NextResponse.redirect(new URL('/dashboard', req.url))
  }

  return NextResponse.next()
})

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/login', '/register'],
}
typescript
// middleware.ts
import { auth } from '@/lib/auth'
import { NextResponse } from 'next/server'

export default auth((req) => {
  const { pathname } = req.nextUrl
  const isAuthenticated = !!req.auth

  // 受保护路由
  if (pathname.startsWith('/dashboard') || pathname.startsWith('/settings')) {
    if (!isAuthenticated) {
      return NextResponse.redirect(new URL('/login', req.url))
    }
  }

  // 已登录用户重定向离开认证页面
  if ((pathname === '/login' || pathname === '/register') && isAuthenticated) {
    return NextResponse.redirect(new URL('/dashboard', req.url))
  }

  return NextResponse.next()
})

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/login', '/register'],
}

Environment Variables

环境变量

bash
undefined
bash
undefined

.env.example

.env.example

─── App ───

─── 应用 ───

NEXT_PUBLIC_APP_URL=http://localhost:3000 NEXTAUTH_SECRET= # openssl rand -base64 32 NEXTAUTH_URL=http://localhost:3000
NEXT_PUBLIC_APP_URL=http://localhost:3000 NEXTAUTH_SECRET= # 使用openssl rand -base64 32生成 NEXTAUTH_URL=http://localhost:3000

─── Database ───

─── 数据库 ───

DATABASE_URL= # postgresql://user:pass@host/db?sslmode=require
DATABASE_URL= # postgresql://user:pass@host/db?sslmode=require

─── OAuth Providers ───

─── OAuth服务商 ───

GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET=

─── Stripe ───

─── Stripe ───

STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_PRO_MONTHLY_PRICE_ID=price_... STRIPE_PRO_YEARLY_PRICE_ID=price_...
STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_PRO_MONTHLY_PRICE_ID=price_... STRIPE_PRO_YEARLY_PRICE_ID=price_...

─── Email ───

─── 邮件 ───

RESEND_API_KEY=re_...
RESEND_API_KEY=re_...

─── Monitoring (optional) ───

─── 监控(可选) ───

SENTRY_DSN=
undefined
SENTRY_DSN=
undefined

Scaffolding Phases

脚手架生成阶段

Execute these phases in order. Validate at the end of each phase.
按顺序执行以下阶段,每个阶段结束后进行验证。

Phase 1: Foundation

阶段1:基础搭建

  1. Initialize Next.js with TypeScript and App Router
  2. Configure Tailwind CSS with custom theme
  3. Install and configure shadcn/ui
  4. Set up ESLint and Prettier
  5. Create
    .env.example
Validate:
pnpm build
completes without errors.
  1. 初始化带TypeScript和App Router的Next.js项目
  2. 配置自定义主题的Tailwind CSS
  3. 安装并配置shadcn/ui
  4. 设置ESLint和Prettier
  5. 创建
    .env.example
    文件
验证:
pnpm build
执行无错误。

Phase 2: Database

阶段2:数据库

  1. Install and configure Drizzle ORM
  2. Write schema (users, accounts, sessions, workspaces, members)
  3. Generate and apply initial migration
  4. Export DB client singleton from
    lib/db.ts
  5. Create seed script with test data
Validate:
pnpm db:push
succeeds and
pnpm db:seed
creates test data.
  1. 安装并配置Drizzle ORM
  2. 编写schema(用户、账号、会话、工作区、成员)
  3. 生成并应用初始迁移文件
  4. lib/db.ts
    导出数据库客户端单例
  5. 创建带测试数据的种子脚本
验证:
pnpm db:push
执行成功,
pnpm db:seed
创建测试数据。

Phase 3: Authentication

阶段3:身份认证

  1. Install and configure NextAuth v5 with Drizzle adapter
  2. Set up OAuth providers (Google, GitHub)
  3. Create auth API route
  4. Implement middleware for route protection
  5. Build login and register pages
Validate: OAuth login works, session persists, protected routes redirect.
  1. 安装并配置带Drizzle适配器的NextAuth v5
  2. 设置OAuth服务商(Google、GitHub)
  3. 创建身份认证API路由
  4. 实现路由保护中间件
  5. 构建登录和注册页面
验证: OAuth登录正常工作,会话持久化,受保护路由正确重定向。

Phase 4: Billing

阶段4:支付集成

  1. Initialize Stripe client
  2. Create checkout session API route
  3. Create customer portal API route
  4. Implement webhook handler with signature verification
  5. Build pricing page and billing settings page
Validate: Complete a test checkout with card
4242 4242 4242 4242
. Verify subscription data written to DB. Replay webhook event and confirm idempotency.
  1. 初始化Stripe客户端
  2. 创建结账会话API路由
  3. 创建客户门户API路由
  4. 实现带签名验证的Webhook处理器
  5. 构建定价页面和支付设置页面
验证: 使用测试卡号
4242 4242 4242 4242
完成测试结账。验证订阅数据写入数据库。重放Webhook事件并确认幂等性。

Phase 5: UI and Polish

阶段5:UI与优化

  1. Build landing page (hero, features, pricing, footer)
  2. Build dashboard layout (sidebar, header, stats)
  3. Build settings pages (profile, billing, team)
  4. Add loading states, error boundaries, and not-found pages
  5. Configure deployment (Vercel/Railway)
Validate:
pnpm build
succeeds. All routes render correctly. No hydration errors.
  1. 构建着陆页(Hero区、功能展示、定价、页脚)
  2. 构建仪表盘布局(侧边栏、头部、统计卡片)
  3. 构建设置页面(个人信息、支付、团队)
  4. 添加加载状态、错误边界和404页面
  5. 配置部署(Vercel/Railway)
验证:
pnpm build
执行成功。所有路由渲染正常。无 hydration 错误。

Multi-Tenancy Patterns

多租户模式

Workspace-Scoped Queries

工作区范围查询

typescript
// Every data query must be scoped to the current workspace
export async function getProjects(workspaceId: string) {
  return db.query.projects.findMany({
    where: eq(projects.workspaceId, workspaceId),
    orderBy: [desc(projects.updatedAt)],
  })
}

// Middleware: resolve workspace from URL or session
export function getCurrentWorkspace(req: Request) {
  // Option A: workspace slug in URL (/workspace/acme/dashboard)
  // Option B: workspace ID in session/cookie
  // Option C: header (X-Workspace-Id) for API calls
}
typescript
// 所有数据查询必须限定在当前工作区内
export async function getProjects(workspaceId: string) {
  return db.query.projects.findMany({
    where: eq(projects.workspaceId, workspaceId),
    orderBy: [desc(projects.updatedAt)],
  })
}

// 中间件:从URL或会话中解析工作区
export function getCurrentWorkspace(req: Request) {
  // 选项A:URL中的工作区slug(/workspace/acme/dashboard)
  // 选项B:会话/ cookie中的工作区ID
  // 选项C:API调用的请求头(X-Workspace-Id)
}

Plan-Based Feature Gating

基于套餐的功能限制

typescript
export function canAccessFeature(workspace: Workspace, feature: string): boolean {
  const PLAN_FEATURES: Record<string, string[]> = {
    free: ['basic_dashboard', 'up_to_3_members'],
    pro: ['advanced_analytics', 'up_to_20_members', 'custom_domain', 'api_access'],
    enterprise: ['sso', 'unlimited_members', 'audit_log', 'sla'],
  }

  const isActive = workspace.stripeCurrentPeriodEnd
    ? workspace.stripeCurrentPeriodEnd > new Date()
    : workspace.plan === 'free'

  if (!isActive) return PLAN_FEATURES.free.includes(feature)
  return PLAN_FEATURES[workspace.plan]?.includes(feature) ?? false
}
typescript
export function canAccessFeature(workspace: Workspace, feature: string): boolean {
  const PLAN_FEATURES: Record<string, string[]> = {
    free: ['basic_dashboard', 'up_to_3_members'],
    pro: ['advanced_analytics', 'up_to_20_members', 'custom_domain', 'api_access'],
    enterprise: ['sso', 'unlimited_members', 'audit_log', 'sla'],
  }

  const isActive = workspace.stripeCurrentPeriodEnd
    ? workspace.stripeCurrentPeriodEnd > new Date()
    : workspace.plan === 'free'

  if (!isActive) return PLAN_FEATURES.free.includes(feature)
  return PLAN_FEATURES[workspace.plan]?.includes(feature) ?? false
}

Common Pitfalls

常见陷阱

  • Missing
    NEXTAUTH_SECRET
    in production
    — causes session errors; generate with
    openssl rand -base64 32
  • Webhook signature verification skipped — always verify Stripe webhook signatures; test with
    stripe listen
  • workspace:*
    in session but not refreshed
    — stale subscription data; recheck on billing pages
  • Edge Runtime conflicts with Drizzle — Drizzle needs Node.js runtime; set
    export const runtime = 'nodejs'
    on API routes
  • No idempotent webhook handling — Stripe may send duplicate events; use
    event.id
    for deduplication
  • Hardcoded Stripe price IDs — store in env vars, not in code; prices change between test and live mode
  • 生产环境缺少
    NEXTAUTH_SECRET
    — 会导致会话错误;使用
    openssl rand -base64 32
    生成
  • 跳过Webhook签名验证 — 务必验证Stripe Webhook签名;使用
    stripe listen
    进行测试
  • 会话中的
    workspace:*
    数据未刷新
    — 订阅数据过时;在支付页面重新检查
  • Edge Runtime与Drizzle冲突 — Drizzle需要Node.js运行时;在API路由中设置
    export const runtime = 'nodejs'
  • Webhook处理器无幂等性 — Stripe可能发送重复事件;使用
    event.id
    进行去重
  • 硬编码Stripe价格ID — 存储在环境变量中,而非代码中;测试和生产环境的价格ID不同

Best Practices

最佳实践

  1. Stripe singleton — create the client once in
    lib/stripe.ts
    , import everywhere
  2. Server actions for form mutations — use Next.js Server Actions instead of API routes for forms
  3. Idempotent webhook handlers — check if the event was already processed before writing to DB
  4. Suspense boundaries for async data — wrap dashboard data in
    <Suspense>
    with loading skeletons
  5. Feature gating at the server level — check
    stripeCurrentPeriodEnd
    on the server, not the client
  6. Rate limiting on auth routes — prevent brute force with Upstash Redis +
    @upstash/ratelimit
  7. Workspace context in every query — never query without scoping to the current workspace
  8. Test with Stripe CLI
    stripe listen --forward-to localhost:3000/api/webhooks/stripe
    for local development
  1. Stripe单例 — 在
    lib/stripe.ts
    中创建一次客户端,在所有地方导入使用
  2. 表单变更使用Server Actions — 对表单使用Next.js Server Actions而非API路由
  3. 幂等Webhook处理器 — 写入数据库前检查事件是否已处理
  4. 异步数据使用Suspense边界 — 将仪表盘数据包裹在
    <Suspense>
    中并添加加载骨架屏
  5. 服务端层面的功能限制 — 在服务端检查
    stripeCurrentPeriodEnd
    ,而非客户端
  6. 认证路由限流 — 使用Upstash Redis +
    @upstash/ratelimit
    防止暴力破解
  7. 所有查询包含工作区上下文 — 永远不要执行未限定当前工作区的查询
  8. 使用Stripe CLI测试 — 本地开发时使用
    stripe listen --forward-to localhost:3000/api/webhooks/stripe

Troubleshooting

故障排查

ProblemCauseSolution
NEXTAUTH_URL
mismatch errors in production
Environment variable not updated from localhost defaultSet
NEXTAUTH_URL
to your actual production domain; omit trailing slash
Stripe webhook returns 400 on every eventRaw body is consumed before signature verificationEnsure the webhook route uses
req.text()
before any JSON parsing; do not use body-parser middleware on the webhook endpoint
Drizzle migrations fail with "relation already exists"Migration was partially applied or schema drifted from migration historyRun
pnpm drizzle-kit drop
to reset the migration journal, then regenerate with
pnpm drizzle-kit generate
and reapply
OAuth callback redirects to wrong URLRedirect URI registered in provider console does not match
NEXTAUTH_URL
Update the authorized redirect URI in Google/GitHub developer console to match your deployment URL exactly
Multi-tenant queries return data from other workspacesMissing
workspaceId
filter in a database query
Audit all
db.query
and
db.select
calls to ensure every query includes a
where
clause scoped to the current workspace
Hydration mismatch on dashboard pagesServer-rendered HTML differs from client due to conditional auth checksMove auth-dependent rendering into client components or wrap with
<Suspense>
; avoid reading session in server components that also render on the client
Stripe test mode charges succeed but live mode failsLive mode price IDs differ from test mode IDsUse separate environment variables for test vs. live Stripe keys and price IDs; verify
.env.production
references the correct live values
问题原因解决方案
生产环境中
NEXTAUTH_URL
不匹配错误
环境变量未从localhost默认值更新
NEXTAUTH_URL
设置为实际生产域名;不要包含末尾斜杠
Stripe Webhook每次事件都返回400签名验证前原始请求体已被消费确保Webhook路由在任何JSON解析前使用
req.text()
;不要在Webhook端点使用body-parser中间件
Drizzle迁移失败,提示“relation already exists”迁移已部分应用或schema与迁移历史不一致运行
pnpm drizzle-kit drop
重置迁移日志,然后使用
pnpm drizzle-kit generate
重新生成并重新应用
OAuth回调重定向到错误URL服务商控制台注册的重定向URI与
NEXTAUTH_URL
不匹配
在Google/GitHub开发者控制台更新授权重定向URI,使其与部署URL完全一致
多租户查询返回其他工作区的数据数据库查询中缺少
workspaceId
过滤条件
审核所有
db.query
db.select
调用,确保每个查询都包含限定当前工作区的
where
子句
仪表盘页面出现Hydration不匹配服务端渲染的HTML因条件认证检查与客户端不同将依赖认证的渲染逻辑移到客户端组件中,或用
<Suspense>
包裹;避免在同时在客户端渲染的服务端组件中读取会话
Stripe测试模式支付成功但生产模式失败生产模式价格ID与测试模式不同为测试和生产环境的Stripe密钥和价格ID使用单独的环境变量;验证
.env.production
引用了正确的生产值

Success Criteria

成功标准

  • Scaffolded project passes
    pnpm build
    with zero errors and zero TypeScript warnings on first run
  • End-to-end authentication flow (register, login, logout, password reset) completes in under 60 seconds of manual testing
  • Stripe checkout creates a subscription and webhook handler updates the database within 5 seconds of payment completion
  • Multi-tenant data isolation verified: queries scoped to Workspace A return zero rows belonging to Workspace B
  • Lighthouse performance score on the landing page is 90+ on mobile with no accessibility violations at the AA level
  • Time from
    git clone
    to running local dev server with seeded data is under 10 minutes following the generated README
  • All environment variables are documented in
    .env.example
    with descriptions, and the app fails fast with clear error messages when required variables are missing
  • 生成的项目首次运行
    pnpm build
    时无错误,无TypeScript警告
  • 端到端认证流程(注册、登录、登出、重置密码)手动测试耗时不超过60秒
  • Stripe结账创建订阅后,Webhook处理器在支付完成后5秒内更新数据库
  • 多租户数据隔离验证:限定工作区A的查询不会返回工作区B的任何数据
  • 着陆页在移动端的Lighthouse性能得分达到90+,且无AA级可访问性违规
  • 按照生成的README,从
    git clone
    到运行带种子数据的本地开发服务器耗时不超过10分钟
  • 所有环境变量在
    .env.example
    中都有文档说明,当缺少必要变量时应用会快速失败并给出清晰错误信息

Scope & Limitations

范围与限制

This skill covers:
  • Full-stack SaaS scaffolding with Next.js App Router, TypeScript, Tailwind, and shadcn/ui
  • Authentication setup with NextAuth v5, Clerk, or Supabase Auth including OAuth and magic link providers
  • Stripe and Lemon Squeezy billing integration with checkout, webhooks, and customer portal
  • Multi-tenancy patterns (workspace/organization) with role-based access and plan-based feature gating
This skill does NOT cover:
  • Ongoing Stripe billing logic beyond initial integration (metered billing, usage-based pricing, invoicing customization) — see
    stripe-integration-expert
  • Database schema design decisions beyond the core tenancy model (complex relational modeling, indexing strategies) — see
    database-schema-designer
  • CI/CD pipeline configuration, deployment automation, or infrastructure provisioning — see
    ci-cd-pipeline-builder
  • API design standards, versioning, or OpenAPI specification generation — see
    api-design-reviewer
本工具涵盖:
  • 基于Next.js App Router、TypeScript、Tailwind和shadcn/ui的全栈SaaS脚手架生成
  • 使用NextAuth v5、Clerk或Supabase Auth的身份认证设置,包括OAuth和魔法链接服务商
  • Stripe和Lemon Squeezy支付集成,包含结账、Webhook和客户门户
  • 多租户模式(工作区/组织),带基于角色的访问控制和基于套餐的功能限制
本工具不涵盖:
  • 初始集成之外的Stripe高级计费逻辑(计量计费、基于使用量的定价、发票定制)—— 请查看
    stripe-integration-expert
  • 核心租户模型之外的数据库schema设计决策(复杂关系建模、索引策略)—— 请查看
    database-schema-designer
  • CI/CD流水线配置、部署自动化或基础设施配置—— 请查看
    ci-cd-pipeline-builder
  • API设计标准、版本控制或OpenAPI规范生成—— 请查看
    api-design-reviewer

Integration Points

集成点

SkillIntegrationData Flow
stripe-integration-expert
Extends the scaffolded Stripe setup with advanced billing patterns (metered, tiered, usage-based)Scaffolder outputs base Stripe config and webhook handler; Stripe expert refines pricing models and adds invoice customization
database-schema-designer
Designs extended schemas beyond the core tenancy tablesScaffolder provides baseline users/workspaces/members schema; schema designer adds domain-specific entities and optimizes indexes
api-design-reviewer
Reviews and improves the generated API routes for consistency and standards complianceScaffolder generates initial API routes; reviewer audits naming, error handling, and response formats
ci-cd-pipeline-builder
Creates deployment pipelines for the scaffolded projectScaffolder outputs the application code; pipeline builder adds GitHub Actions, preview deployments, and production release workflows
env-secrets-manager
Audits and secures the environment variable configurationScaffolder generates
.env.example
; secrets manager validates no secrets are hardcoded and recommends vault integration
observability-designer
Adds logging, tracing, and monitoring to the scaffolded applicationScaffolder provides the application structure; observability designer instruments API routes, webhooks, and auth flows
工具集成方式数据流
stripe-integration-expert
在生成的基础Stripe设置上扩展高级计费模式(计量、分层、基于使用量)脚手架生成器输出基础Stripe配置和Webhook处理器;Stripe专家优化定价模型并添加发票定制
database-schema-designer
设计核心租户表之外的扩展schema脚手架生成器提供基础的用户/工作区/成员schema;schema设计师添加领域特定实体并优化索引
api-design-reviewer
审核并改进生成的API路由,确保一致性和标准合规性脚手架生成器生成初始API路由;审核者检查命名、错误处理和响应格式
ci-cd-pipeline-builder
为生成的项目创建部署流水线脚手架生成器输出应用代码;流水线构建器添加GitHub Actions、预览部署和生产发布工作流
env-secrets-manager
审核并保护环境变量配置脚手架生成器生成
.env.example
;密钥管理器验证无硬编码密钥并推荐使用密钥库集成
observability-designer
为生成的应用添加日志、追踪和监控脚手架生成器提供应用结构;可观测性设计师为API路由、Webhook和认证流程添加监控