saas-scaffolder
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSaaS 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.tsmy-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.tsDatabase 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
undefinedbash
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=
undefinedSENTRY_DSN=
undefinedScaffolding Phases
脚手架生成阶段
Execute these phases in order. Validate at the end of each phase.
按顺序执行以下阶段,每个阶段结束后进行验证。
Phase 1: Foundation
阶段1:基础搭建
- Initialize Next.js with TypeScript and App Router
- Configure Tailwind CSS with custom theme
- Install and configure shadcn/ui
- Set up ESLint and Prettier
- Create
.env.example
Validate: completes without errors.
pnpm build- 初始化带TypeScript和App Router的Next.js项目
- 配置自定义主题的Tailwind CSS
- 安装并配置shadcn/ui
- 设置ESLint和Prettier
- 创建文件
.env.example
验证: 执行无错误。
pnpm buildPhase 2: Database
阶段2:数据库
- Install and configure Drizzle ORM
- Write schema (users, accounts, sessions, workspaces, members)
- Generate and apply initial migration
- Export DB client singleton from
lib/db.ts - Create seed script with test data
Validate: succeeds and creates test data.
pnpm db:pushpnpm db:seed- 安装并配置Drizzle ORM
- 编写schema(用户、账号、会话、工作区、成员)
- 生成并应用初始迁移文件
- 从导出数据库客户端单例
lib/db.ts - 创建带测试数据的种子脚本
验证: 执行成功,创建测试数据。
pnpm db:pushpnpm db:seedPhase 3: Authentication
阶段3:身份认证
- Install and configure NextAuth v5 with Drizzle adapter
- Set up OAuth providers (Google, GitHub)
- Create auth API route
- Implement middleware for route protection
- Build login and register pages
Validate: OAuth login works, session persists, protected routes redirect.
- 安装并配置带Drizzle适配器的NextAuth v5
- 设置OAuth服务商(Google、GitHub)
- 创建身份认证API路由
- 实现路由保护中间件
- 构建登录和注册页面
验证: OAuth登录正常工作,会话持久化,受保护路由正确重定向。
Phase 4: Billing
阶段4:支付集成
- Initialize Stripe client
- Create checkout session API route
- Create customer portal API route
- Implement webhook handler with signature verification
- Build pricing page and billing settings page
Validate: Complete a test checkout with card . Verify subscription data written to DB. Replay webhook event and confirm idempotency.
4242 4242 4242 4242- 初始化Stripe客户端
- 创建结账会话API路由
- 创建客户门户API路由
- 实现带签名验证的Webhook处理器
- 构建定价页面和支付设置页面
验证: 使用测试卡号完成测试结账。验证订阅数据写入数据库。重放Webhook事件并确认幂等性。
4242 4242 4242 4242Phase 5: UI and Polish
阶段5:UI与优化
- Build landing page (hero, features, pricing, footer)
- Build dashboard layout (sidebar, header, stats)
- Build settings pages (profile, billing, team)
- Add loading states, error boundaries, and not-found pages
- Configure deployment (Vercel/Railway)
Validate: succeeds. All routes render correctly. No hydration errors.
pnpm build- 构建着陆页(Hero区、功能展示、定价、页脚)
- 构建仪表盘布局(侧边栏、头部、统计卡片)
- 构建设置页面(个人信息、支付、团队)
- 添加加载状态、错误边界和404页面
- 配置部署(Vercel/Railway)
验证: 执行成功。所有路由渲染正常。无 hydration 错误。
pnpm buildMulti-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 in production — causes session errors; generate with
NEXTAUTH_SECRETopenssl rand -base64 32 - Webhook signature verification skipped — always verify Stripe webhook signatures; test with
stripe listen - in session but not refreshed — stale subscription data; recheck on billing pages
workspace:* - Edge Runtime conflicts with Drizzle — Drizzle needs Node.js runtime; set on API routes
export const runtime = 'nodejs' - No idempotent webhook handling — Stripe may send duplicate events; use for deduplication
event.id - 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
最佳实践
- Stripe singleton — create the client once in , import everywhere
lib/stripe.ts - Server actions for form mutations — use Next.js Server Actions instead of API routes for forms
- Idempotent webhook handlers — check if the event was already processed before writing to DB
- Suspense boundaries for async data — wrap dashboard data in with loading skeletons
<Suspense> - Feature gating at the server level — check on the server, not the client
stripeCurrentPeriodEnd - Rate limiting on auth routes — prevent brute force with Upstash Redis +
@upstash/ratelimit - Workspace context in every query — never query without scoping to the current workspace
- Test with Stripe CLI — for local development
stripe listen --forward-to localhost:3000/api/webhooks/stripe
- Stripe单例 — 在中创建一次客户端,在所有地方导入使用
lib/stripe.ts - 表单变更使用Server Actions — 对表单使用Next.js Server Actions而非API路由
- 幂等Webhook处理器 — 写入数据库前检查事件是否已处理
- 异步数据使用Suspense边界 — 将仪表盘数据包裹在中并添加加载骨架屏
<Suspense> - 服务端层面的功能限制 — 在服务端检查,而非客户端
stripeCurrentPeriodEnd - 认证路由限流 — 使用Upstash Redis + 防止暴力破解
@upstash/ratelimit - 所有查询包含工作区上下文 — 永远不要执行未限定当前工作区的查询
- 使用Stripe CLI测试 — 本地开发时使用
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Troubleshooting
故障排查
| Problem | Cause | Solution |
|---|---|---|
| Environment variable not updated from localhost default | Set |
| Stripe webhook returns 400 on every event | Raw body is consumed before signature verification | Ensure the webhook route uses |
| Drizzle migrations fail with "relation already exists" | Migration was partially applied or schema drifted from migration history | Run |
| OAuth callback redirects to wrong URL | Redirect URI registered in provider console does not match | Update the authorized redirect URI in Google/GitHub developer console to match your deployment URL exactly |
| Multi-tenant queries return data from other workspaces | Missing | Audit all |
| Hydration mismatch on dashboard pages | Server-rendered HTML differs from client due to conditional auth checks | Move auth-dependent rendering into client components or wrap with |
| Stripe test mode charges succeed but live mode fails | Live mode price IDs differ from test mode IDs | Use separate environment variables for test vs. live Stripe keys and price IDs; verify |
| 问题 | 原因 | 解决方案 |
|---|---|---|
生产环境中 | 环境变量未从localhost默认值更新 | 将 |
| Stripe Webhook每次事件都返回400 | 签名验证前原始请求体已被消费 | 确保Webhook路由在任何JSON解析前使用 |
| Drizzle迁移失败,提示“relation already exists” | 迁移已部分应用或schema与迁移历史不一致 | 运行 |
| OAuth回调重定向到错误URL | 服务商控制台注册的重定向URI与 | 在Google/GitHub开发者控制台更新授权重定向URI,使其与部署URL完全一致 |
| 多租户查询返回其他工作区的数据 | 数据库查询中缺少 | 审核所有 |
| 仪表盘页面出现Hydration不匹配 | 服务端渲染的HTML因条件认证检查与客户端不同 | 将依赖认证的渲染逻辑移到客户端组件中,或用 |
| Stripe测试模式支付成功但生产模式失败 | 生产模式价格ID与测试模式不同 | 为测试和生产环境的Stripe密钥和价格ID使用单独的环境变量;验证 |
Success Criteria
成功标准
- Scaffolded project passes with zero errors and zero TypeScript warnings on first run
pnpm build - 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 to running local dev server with seeded data is under 10 minutes following the generated README
git clone - All environment variables are documented in with descriptions, and the app fails fast with clear error messages when required variables are missing
.env.example
- 生成的项目首次运行时无错误,无TypeScript警告
pnpm build - 端到端认证流程(注册、登录、登出、重置密码)手动测试耗时不超过60秒
- Stripe结账创建订阅后,Webhook处理器在支付完成后5秒内更新数据库
- 多租户数据隔离验证:限定工作区A的查询不会返回工作区B的任何数据
- 着陆页在移动端的Lighthouse性能得分达到90+,且无AA级可访问性违规
- 按照生成的README,从到运行带种子数据的本地开发服务器耗时不超过10分钟
git clone - 所有环境变量在中都有文档说明,当缺少必要变量时应用会快速失败并给出清晰错误信息
.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
集成点
| Skill | Integration | Data Flow |
|---|---|---|
| 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 |
| Designs extended schemas beyond the core tenancy tables | Scaffolder provides baseline users/workspaces/members schema; schema designer adds domain-specific entities and optimizes indexes |
| Reviews and improves the generated API routes for consistency and standards compliance | Scaffolder generates initial API routes; reviewer audits naming, error handling, and response formats |
| Creates deployment pipelines for the scaffolded project | Scaffolder outputs the application code; pipeline builder adds GitHub Actions, preview deployments, and production release workflows |
| Audits and secures the environment variable configuration | Scaffolder generates |
| Adds logging, tracing, and monitoring to the scaffolded application | Scaffolder provides the application structure; observability designer instruments API routes, webhooks, and auth flows |
| 工具 | 集成方式 | 数据流 |
|---|---|---|
| 在生成的基础Stripe设置上扩展高级计费模式(计量、分层、基于使用量) | 脚手架生成器输出基础Stripe配置和Webhook处理器;Stripe专家优化定价模型并添加发票定制 |
| 设计核心租户表之外的扩展schema | 脚手架生成器提供基础的用户/工作区/成员schema;schema设计师添加领域特定实体并优化索引 |
| 审核并改进生成的API路由,确保一致性和标准合规性 | 脚手架生成器生成初始API路由;审核者检查命名、错误处理和响应格式 |
| 为生成的项目创建部署流水线 | 脚手架生成器输出应用代码;流水线构建器添加GitHub Actions、预览部署和生产发布工作流 |
| 审核并保护环境变量配置 | 脚手架生成器生成 |
| 为生成的应用添加日志、追踪和监控 | 脚手架生成器提供应用结构;可观测性设计师为API路由、Webhook和认证流程添加监控 |