saas-scaffolder
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSaaS Scaffolder
SaaS项目脚手架
Tier: POWERFUL
Category: Product Team
Domain: Full-Stack Development / Project Bootstrapping
Category: Product Team
Domain: Full-Stack Development / Project Bootstrapping
等级:强大级
类别:产品团队
领域:全栈开发 / 项目启动
类别:产品团队
领域:全栈开发 / 项目启动
Input Format
输入格式
Product: [name]
Description: [1-3 sentences]
Auth: nextauth | clerk | supabase
Database: neondb | supabase | planetscale
Payments: stripe | lemonsqueezy | none
Features: [comma-separated list]Product: [name]
Description: [1-3 sentences]
Auth: nextauth | clerk | supabase
Database: neondb | supabase | planetscale
Payments: stripe | lemonsqueezy | none
Features: [comma-separated list]File Tree Output
文件树输出
my-saas/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ ├── register/page.tsx
│ │ └── layout.tsx
│ ├── (dashboard)/
│ │ ├── dashboard/page.tsx
│ │ ├── settings/page.tsx
│ │ ├── billing/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
│ │ └── billing/portal/route.ts
│ └── layout.tsx
├── components/
│ ├── ui/
│ ├── auth/
│ │ ├── login-form.tsx
│ │ └── register-form.tsx
│ ├── dashboard/
│ │ ├── sidebar.tsx
│ │ ├── header.tsx
│ │ └── stats-card.tsx
│ ├── marketing/
│ │ ├── hero.tsx
│ │ ├── features.tsx
│ │ ├── pricing.tsx
│ │ └── footer.tsx
│ └── billing/
│ ├── plan-card.tsx
│ └── usage-meter.tsx
├── lib/
│ ├── auth.ts
│ ├── db.ts
│ ├── stripe.ts
│ ├── validations.ts
│ └── utils.ts
├── db/
│ ├── schema.ts
│ └── migrations/
├── hooks/
│ ├── use-subscription.ts
│ └── use-user.ts
├── types/index.ts
├── middleware.ts
├── .env.example
├── drizzle.config.ts
└── next.config.tsmy-saas/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ ├── register/page.tsx
│ │ └── layout.tsx
│ ├── (dashboard)/
│ │ ├── dashboard/page.tsx
│ │ ├── settings/page.tsx
│ │ ├── billing/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
│ │ └── billing/portal/route.ts
│ └── layout.tsx
├── components/
│ ├── ui/
│ ├── auth/
│ │ ├── login-form.tsx
│ │ └── register-form.tsx
│ ├── dashboard/
│ │ ├── sidebar.tsx
│ │ ├── header.tsx
│ │ └── stats-card.tsx
│ ├── marketing/
│ │ ├── hero.tsx
│ │ ├── features.tsx
│ │ ├── pricing.tsx
│ │ └── footer.tsx
│ └── billing/
│ ├── plan-card.tsx
│ └── usage-meter.tsx
├── lib/
│ ├── auth.ts
│ ├── db.ts
│ ├── stripe.ts
│ ├── validations.ts
│ └── utils.ts
├── db/
│ ├── schema.ts
│ └── migrations/
├── hooks/
│ ├── use-subscription.ts
│ └── use-user.ts
├── types/index.ts
├── middleware.ts
├── .env.example
├── drizzle.config.ts
└── next.config.tsKey Component Patterns
核心组件模式
Auth Config (NextAuth)
认证配置(NextAuth)
typescript
// lib/auth.ts
import { NextAuthOptions } from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { db } from "./db"
export const authOptions: NextAuthOptions = {
adapter: DrizzleAdapter(db),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
session: async ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
subscriptionStatus: user.subscriptionStatus,
},
}),
},
pages: { signIn: "/login" },
}typescript
// lib/auth.ts
import { NextAuthOptions } from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { db } from "./db"
export const authOptions: NextAuthOptions = {
adapter: DrizzleAdapter(db),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
session: async ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
subscriptionStatus: user.subscriptionStatus,
},
}),
},
pages: { signIn: "/login" },
}Database Schema (Drizzle + NeonDB)
数据库Schema(Drizzle + NeonDB)
typescript
// db/schema.ts
import { pgTable, text, timestamp, integer } from "drizzle-orm/pg-core"
export const users = pgTable("users", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").notNull().unique(),
emailVerified: timestamp("emailVerified"),
image: text("image"),
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").defaultNow().notNull(),
})
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(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
})typescript
// db/schema.ts
import { pgTable, text, timestamp, integer } from "drizzle-orm/pg-core"
export const users = pgTable("users", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").notNull().unique(),
emailVerified: timestamp("emailVerified"),
image: text("image"),
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").defaultNow().notNull(),
})
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(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
})Stripe Checkout Route
Stripe结账路由
typescript
// app/api/billing/checkout/route.ts
import { NextResponse } from "next/server"
import { getServerSession } from "next-auth"
import { authOptions } from "@/lib/auth"
import { stripe } from "@/lib/stripe"
import { db } from "@/lib/db"
import { users } from "@/db/schema"
import { eq } from "drizzle-orm"
export async function POST(req: Request) {
const session = await getServerSession(authOptions)
if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const { priceId } = await req.json()
const [user] = await db.select().from(users).where(eq(users.id, session.user.id))
let customerId = user.stripeCustomerId
if (!customerId) {
const customer = await stripe.customers.create({ email: session.user.email! })
customerId = customer.id
await db.update(users).set({ stripeCustomerId: customerId }).where(eq(users.id, user.id))
}
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}/dashboard?upgraded=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
subscription_data: { trial_period_days: 14 },
})
return NextResponse.json({ url: checkoutSession.url })
}typescript
// app/api/billing/checkout/route.ts
import { NextResponse } from "next/server"
import { getServerSession } from "next-auth"
import { authOptions } from "@/lib/auth"
import { stripe } from "@/lib/stripe"
import { db } from "@/lib/db"
import { users } from "@/db/schema"
import { eq } from "drizzle-orm"
export async function POST(req: Request) {
const session = await getServerSession(authOptions)
if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const { priceId } = await req.json()
const [user] = await db.select().from(users).where(eq(users.id, session.user.id))
let customerId = user.stripeCustomerId
if (!customerId) {
const customer = await stripe.customers.create({ email: session.user.email! })
customerId = customer.id
await db.update(users).set({ stripeCustomerId: customerId }).where(eq(users.id, user.id))
}
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}/dashboard?upgraded=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
subscription_data: { trial_period_days: 14 },
})
return NextResponse.json({ url: checkoutSession.url })
}Middleware
中间件
typescript
// middleware.ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"
export default withAuth(
function middleware(req) {
const token = req.nextauth.token
if (req.nextUrl.pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", req.url))
}
},
{ callbacks: { authorized: ({ token }) => !!token } }
)
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*", "/billing/:path*"],
}typescript
// middleware.ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"
export default withAuth(
function middleware(req) {
const token = req.nextauth.token
if (req.nextUrl.pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", req.url))
}
},
{ callbacks: { authorized: ({ token }) => !!token } }
)
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*", "/billing/:path*"],
}Environment Variables Template
环境变量模板
bash
undefinedbash
undefined.env.example
.env.example
NEXT_PUBLIC_APP_URL=http://localhost:3000
DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require
NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32
NEXTAUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_PRO_PRICE_ID=price_...
---NEXT_PUBLIC_APP_URL=http://localhost:3000
DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require
NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32
NEXTAUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_PRO_PRICE_ID=price_...
---Scaffold Checklist
脚手架检查清单
The following phases must be completed in order. Validate at the end of each phase before proceeding.
请按顺序完成以下阶段。每个阶段结束后必须验证通过,再进入下一阶段。
Phase 1 — Foundation
阶段1 — 基础搭建
- 1. Next.js initialized with TypeScript and App Router
- 2. Tailwind CSS configured with custom theme tokens
- 3. shadcn/ui installed and configured
- 4. ESLint + Prettier configured
- 5. created with all required variables
.env.example
✅ Validate: Run — no TypeScript or lint errors should appear.
🔧 If build fails: Check paths and that all shadcn/ui peer dependencies are installed.
npm run build🔧 If build fails: Check
tsconfig.json- 1. 初始化带有TypeScript和App Router的Next.js项目
- 2. 配置带有自定义主题令牌的Tailwind CSS
- 3. 安装并配置shadcn/ui
- 4. 配置ESLint + Prettier
- 5. 创建包含所有必要变量的
.env.example
✅ 验证: 运行 — 不应出现TypeScript或语法检查错误。
🔧 构建失败排查: 检查路径,确保所有shadcn/ui的peer依赖已安装。
npm run build🔧 构建失败排查: 检查
tsconfig.jsonPhase 2 — Database
阶段2 — 数据库
- 6. Drizzle ORM installed and configured
- 7. Schema written (users, accounts, sessions, verification_tokens)
- 8. Initial migration generated and applied
- 9. DB client singleton exported from
lib/db.ts - 10. DB connection tested in local environment
✅ Validate: Run a simple in a test script — it should return an empty array without throwing.
🔧 If DB connection fails: Verify format includes for NeonDB/Supabase. Check that the migration has been applied with (dev) or (prod).
db.select().from(users)🔧 If DB connection fails: Verify
DATABASE_URL?sslmode=requiredrizzle-kit pushdrizzle-kit migrate- 6. 安装并配置Drizzle ORM
- 7. 编写Schema(用户、账户、会话、验证令牌)
- 8. 生成并应用初始迁移文件
- 9. 从导出DB客户端单例
lib/db.ts - 10. 在本地环境测试DB连接
✅ 验证: 在测试脚本中运行简单的 — 应返回空数组且无报错。
🔧 DB连接失败排查: 验证格式是否包含(适用于NeonDB/Supabase)。检查是否已通过(开发环境)或(生产环境)应用迁移。
db.select().from(users)🔧 DB连接失败排查: 验证
DATABASE_URL?sslmode=requiredrizzle-kit pushdrizzle-kit migratePhase 3 — Authentication
阶段3 — 身份认证
- 11. Auth provider installed (NextAuth / Clerk / Supabase)
- 12. OAuth provider configured (Google / GitHub)
- 13. Auth API route created
- 14. Session callback adds user ID and subscription status
- 15. Middleware protects dashboard routes
- 16. Login and register pages built with error states
✅ Validate: Sign in via OAuth, confirm session user has and . Attempt to access without a session — you should be redirected to .
🔧 If sign-out loops occur in production: Ensure is set and consistent across deployments. Add to extend session types if TypeScript errors appear.
idsubscriptionStatus/dashboard/login🔧 If sign-out loops occur in production: Ensure
NEXTAUTH_SECRETdeclare module "next-auth"- 11. 安装认证提供商(NextAuth / Clerk / Supabase)
- 12. 配置OAuth提供商(Google / GitHub)
- 13. 创建认证API路由
- 14. 会话回调添加用户ID和订阅状态
- 15. 中间件保护仪表盘路由
- 16. 构建包含错误状态的登录和注册页面
✅ 验证: 通过OAuth登录,确认会话用户包含和。尝试在无会话状态下访问 — 应被重定向至。
🔧 生产环境登出循环排查: 确保已设置且在所有部署环境中保持一致。若出现TypeScript错误,添加以扩展会话类型。
idsubscriptionStatus/dashboard/login🔧 生产环境登出循环排查: 确保
NEXTAUTH_SECRETdeclare module "next-auth"Phase 4 — Payments
阶段4 — 支付功能
- 17. Stripe client initialized with TypeScript types
- 18. Checkout session route created
- 19. Customer portal route created
- 20. Stripe webhook handler with signature verification
- 21. Webhook updates user subscription status in DB idempotently
✅ Validate: Complete a Stripe test checkout using a card. Confirm is written to the DB. Replay the webhook event and confirm idempotency (no duplicate DB writes).
🔧 If webhook signature fails: Use locally — never hardcode the raw webhook secret. Verify matches the listener output.
4242 4242 4242 4242stripeSubscriptionIdcheckout.session.completed🔧 If webhook signature fails: Use
stripe listen --forward-to localhost:3000/api/webhooks/stripeSTRIPE_WEBHOOK_SECRET- 17. 初始化带有TypeScript类型的Stripe客户端
- 18. 创建结账会话路由
- 19. 创建客户门户路由
- 20. 带有签名验证的Stripe webhook处理器
- 21. Webhook幂等性更新数据库中的用户订阅状态
✅ 验证: 使用测试卡号完成Stripe测试结账。确认已写入数据库。重放 webhook事件,确认幂等性(无重复数据库写入)。
🔧 Webhook签名验证失败排查: 本地使用 — 切勿硬编码原始webhook密钥。验证是否与监听器输出一致。
4242 4242 4242 4242stripeSubscriptionIdcheckout.session.completed🔧 Webhook签名验证失败排查: 本地使用
stripe listen --forward-to localhost:3000/api/webhooks/stripeSTRIPE_WEBHOOK_SECRETPhase 5 — UI
阶段5 — 用户界面
- 22. Landing page with hero, features, pricing sections
- 23. Dashboard layout with sidebar and responsive header
- 24. Billing page showing current plan and upgrade options
- 25. Settings page with profile update form and success states
✅ Validate: Run for a final production build check. Navigate all routes manually and confirm no broken layouts, missing session data, or hydration errors.
npm run build- 22. 包含英雄区、功能区、定价区的着陆页
- 23. 带有侧边栏和响应式头部的仪表盘布局
- 24. 显示当前套餐和升级选项的账单页面
- 25. 包含个人资料更新表单和成功状态的设置页面
✅ 验证: 运行进行最终生产构建检查。手动导航所有路由,确认无布局断裂、会话数据缺失或 hydration 错误。
npm run buildReference Files
参考文件
For additional guidance, generate the following companion reference files alongside the scaffold:
- — Auth providers, database options, ORM alternatives, payment providers, UI themes, and billing models (per-seat, flat-rate, usage-based).
CUSTOMIZATION.md - — Common failure modes: missing
PITFALLS.md, webhook secret mismatches, Edge runtime conflicts with Drizzle, unextended session types, and migration strategy differences between dev and prod.NEXTAUTH_SECRET - — Stripe singleton pattern, server actions for form mutations, idempotent webhook handlers,
BEST_PRACTICES.mdboundaries for async dashboard data, server-side feature gating viaSuspense, and rate limiting on auth routes with Upstash Redis +stripeCurrentPeriodEnd.@upstash/ratelimit
如需更多指导,可在生成脚手架的同时生成以下配套参考文件:
- — 认证提供商、数据库选项、ORM替代方案、支付提供商、UI主题和计费模式(按席位、固定费率、基于使用量)。
CUSTOMIZATION.md - — 常见失败模式:缺失
PITFALLS.md、webhook密钥不匹配、Drizzle与Edge运行时冲突、未扩展会话类型、开发与生产环境迁移策略差异。NEXTAUTH_SECRET - — Stripe单例模式、表单修改的服务器操作、幂等webhook处理器、异步仪表盘数据的
BEST_PRACTICES.md边界、通过Suspense进行服务器端功能管控、使用Upstash Redis +stripeCurrentPeriodEnd对认证路由进行限流。@upstash/ratelimit