saas-scaffolder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SaaS Scaffolder

SaaS项目脚手架

Tier: POWERFUL
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.ts

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.ts

Key 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
undefined
bash
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.
    .env.example
    created with all required variables
Validate: Run
npm run build
— no TypeScript or lint errors should appear.
🔧 If build fails: Check
tsconfig.json
paths and that all shadcn/ui peer dependencies are installed.
  • 1. 初始化带有TypeScript和App Router的Next.js项目
  • 2. 配置带有自定义主题令牌的Tailwind CSS
  • 3. 安装并配置shadcn/ui
  • 4. 配置ESLint + Prettier
  • 5. 创建包含所有必要变量的
    .env.example
验证: 运行
npm run build
— 不应出现TypeScript或语法检查错误。
🔧 构建失败排查: 检查
tsconfig.json
路径,确保所有shadcn/ui的peer依赖已安装。

Phase 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
db.select().from(users)
in a test script — it should return an empty array without throwing.
🔧 If DB connection fails: Verify
DATABASE_URL
format includes
?sslmode=require
for NeonDB/Supabase. Check that the migration has been applied with
drizzle-kit push
(dev) or
drizzle-kit migrate
(prod).
  • 6. 安装并配置Drizzle ORM
  • 7. 编写Schema(用户、账户、会话、验证令牌)
  • 8. 生成并应用初始迁移文件
  • 9. 从
    lib/db.ts
    导出DB客户端单例
  • 10. 在本地环境测试DB连接
验证: 在测试脚本中运行简单的
db.select().from(users)
— 应返回空数组且无报错。
🔧 DB连接失败排查: 验证
DATABASE_URL
格式是否包含
?sslmode=require
(适用于NeonDB/Supabase)。检查是否已通过
drizzle-kit push
(开发环境)或
drizzle-kit migrate
(生产环境)应用迁移。

Phase 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
id
and
subscriptionStatus
. Attempt to access
/dashboard
without a session — you should be redirected to
/login
.
🔧 If sign-out loops occur in production: Ensure
NEXTAUTH_SECRET
is set and consistent across deployments. Add
declare module "next-auth"
to extend session types if TypeScript errors appear.
  • 11. 安装认证提供商(NextAuth / Clerk / Supabase)
  • 12. 配置OAuth提供商(Google / GitHub)
  • 13. 创建认证API路由
  • 14. 会话回调添加用户ID和订阅状态
  • 15. 中间件保护仪表盘路由
  • 16. 构建包含错误状态的登录和注册页面
验证: 通过OAuth登录,确认会话用户包含
id
subscriptionStatus
。尝试在无会话状态下访问
/dashboard
— 应被重定向至
/login

🔧 生产环境登出循环排查: 确保
NEXTAUTH_SECRET
已设置且在所有部署环境中保持一致。若出现TypeScript错误,添加
declare 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
4242 4242 4242 4242
card. Confirm
stripeSubscriptionId
is written to the DB. Replay the
checkout.session.completed
webhook event and confirm idempotency (no duplicate DB writes).
🔧 If webhook signature fails: Use
stripe listen --forward-to localhost:3000/api/webhooks/stripe
locally — never hardcode the raw webhook secret. Verify
STRIPE_WEBHOOK_SECRET
matches the listener output.
  • 17. 初始化带有TypeScript类型的Stripe客户端
  • 18. 创建结账会话路由
  • 19. 创建客户门户路由
  • 20. 带有签名验证的Stripe webhook处理器
  • 21. Webhook幂等性更新数据库中的用户订阅状态
验证: 使用测试卡号
4242 4242 4242 4242
完成Stripe测试结账。确认
stripeSubscriptionId
已写入数据库。重放
checkout.session.completed
webhook事件,确认幂等性(无重复数据库写入)。
🔧 Webhook签名验证失败排查: 本地使用
stripe listen --forward-to localhost:3000/api/webhooks/stripe
— 切勿硬编码原始webhook密钥。验证
STRIPE_WEBHOOK_SECRET
是否与监听器输出一致。

Phase 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
npm run build
for a final production build check. Navigate all routes manually and confirm no broken layouts, missing session data, or hydration errors.

  • 22. 包含英雄区、功能区、定价区的着陆页
  • 23. 带有侧边栏和响应式头部的仪表盘布局
  • 24. 显示当前套餐和升级选项的账单页面
  • 25. 包含个人资料更新表单和成功状态的设置页面
验证: 运行
npm run build
进行最终生产构建检查。手动导航所有路由,确认无布局断裂、会话数据缺失或 hydration 错误。

Reference Files

参考文件

For additional guidance, generate the following companion reference files alongside the scaffold:
  • CUSTOMIZATION.md
    — Auth providers, database options, ORM alternatives, payment providers, UI themes, and billing models (per-seat, flat-rate, usage-based).
  • PITFALLS.md
    — Common failure modes: missing
    NEXTAUTH_SECRET
    , webhook secret mismatches, Edge runtime conflicts with Drizzle, unextended session types, and migration strategy differences between dev and prod.
  • BEST_PRACTICES.md
    — Stripe singleton pattern, server actions for form mutations, idempotent webhook handlers,
    Suspense
    boundaries for async dashboard data, server-side feature gating via
    stripeCurrentPeriodEnd
    , and rate limiting on auth routes with Upstash Redis +
    @upstash/ratelimit
    .
如需更多指导,可在生成脚手架的同时生成以下配套参考文件:
  • CUSTOMIZATION.md
    — 认证提供商、数据库选项、ORM替代方案、支付提供商、UI主题和计费模式(按席位、固定费率、基于使用量)。
  • PITFALLS.md
    — 常见失败模式:缺失
    NEXTAUTH_SECRET
    、webhook密钥不匹配、Drizzle与Edge运行时冲突、未扩展会话类型、开发与生产环境迁移策略差异。
  • BEST_PRACTICES.md
    — Stripe单例模式、表单修改的服务器操作、幂等webhook处理器、异步仪表盘数据的
    Suspense
    边界、通过
    stripeCurrentPeriodEnd
    进行服务器端功能管控、使用Upstash Redis +
    @upstash/ratelimit
    对认证路由进行限流。