关于 saas-scaffolder
面向产品团队的 SaaS 项目脚手架技能,指导分阶段构建 Next.js 和 TypeScript 应用。它支持选择认证、数据库和支付方案,并在各阶段提供验证检查。
主要功能
- 01生成 SaaS 项目结构
- 02配置 Next.js、TypeScript 和 App Router
- 03选择认证与数据库方案
- 04设置支付路由和 Webhook
- 05通过构建及行为检查验证各阶段
适用场景
- 01根据产品描述启动 SaaS 应用
- 02构建认证功能和受保护的仪表板
- 03添加订阅结账与账单页面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ alirezarezvani/ claude-skills --skill saas-scaffolder安装后在 Claude Code 中输入/saas-scaffolder确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/architecture-patterns.mdauth-billing-guide.mdsaas-architecture-patterns.mdtech-stack-comparison.mdscripts/project_bootstrapper.py详细介绍
SaaS Scaffolder
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]
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
Key Component Patterns
Auth Config (NextAuth)
// 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)
// 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
// 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
// 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
# .env.example
NEXT_PUBLIC_APP_URL=http://localhost:3000
DATABASE_URL=postgresql://user:[email protected]/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
以下各阶段必须按顺序完成。每个阶段结束时先进行验证,再继续下一阶段。
Phase 1 — Foundation
- 1. 使用 TypeScript 和 App Router 初始化 Next.js
- 2. 配置带有自定义主题令牌的 Tailwind CSS
- 3. 安装并配置 shadcn/ui
- 4. 配置 ESLint + Prettier
- 5. 创建包含所有必需变量的
.env.example
✅ Validate: 运行 npm run build,不应出现 TypeScript 或 lint 错误。
🔧 If build fails: 检查 tsconfig.json 的 paths 配置,并确认已安装所有 shadcn/ui 的 peer dependencies。
Phase 2 — Database
- 6. 安装并配置 Drizzle ORM
- 7. 编写 Schema(users、accounts、sessions、verification_tokens)
- 8. 生成并应用初始迁移
- 9. 从
lib/db.ts导出 DB 客户端单例 - 10. 在本地环境中测试数据库连接
✅ Validate: 在测试脚本中运行一个简单的 db.select().from(users),它应返回空数组且不抛出异常。
🔧 If DB connection fails: 确认 NeonDB/Supabase 的 DATABASE_URL 格式包含 ?sslmode=require。检查迁移是否已通过 drizzle-kit push(开发环境)或 drizzle-kit migrate(生产环境)应用。
Phase 3 — Authentication
- 11. 安装认证提供方(NextAuth / Clerk / Supabase)
- 12. 配置 OAuth 提供方(Google / GitHub)
- 13. 创建认证 API 路由
- 14. Session 回调中添加用户 ID 和订阅状态
- 15. Middleware 保护仪表盘路由
- 16. 构建带错误状态的登录与注册页面
✅ Validate: 通过 OAuth 登录,确认 session 用户包含 id 和 subscriptionStatus。在没有 session 的情况下访问 /dashboard,应被重定向到 /login。
🔧 If sign-out loops occur in production: 确保 NEXTAUTH_SECRET 已设置且在各次部署中保持一致。如果出现 TypeScript 错误,添加 declare module "next-auth" 以扩展 session 类型。
Phase 4 — Payments
- 17. 初始化带有 TypeScript 类型的 Stripe 客户端
- 18. 创建 Checkout session 路由
- 19. 创建客户门户路由
- 20. 编写带签名校验的 Stripe webhook 处理程序
- 21. Webhook 以幂等方式更新数据库中的用户订阅状态
✅ Validate: 使用 4242 4242 4242 4242 测试卡完成一次 Stripe 测试结账。确认 stripeSubscriptionId 已写入数据库。重放 checkout.session.completed webhook 事件,确认具有幂等性(不会产生重复的数据库写入)。
🔧 If webhook signature fails: 本地使用 stripe listen --forward-to localhost:3000/api/webhooks/stripe,切勿硬编码原始 webhook 密钥。确认 STRIPE_WEBHOOK_SECRET 与监听器输出一致。
Phase 5 — UI
- 22. 包含 hero、功能与定价板块的落地页
- 23. 带侧边栏和响应式头部的仪表盘布局
- 24. 显示当前套餐和升级选项的账单页面
- 25. 包含个人资料更新表单和成功状态的设置页面
✅ Validate: 运行 npm run build 进行最终生产构建检查。手动浏览所有路由,确认没有布局错乱、session 数据缺失或 hydration 错误。
参考文件
如需更多指导,请在生成脚手架的同时生成以下配套参考文件:
CUSTOMIZATION.md:认证提供方、数据库选项、ORM 替代方案、支付提供方、UI 主题,以及计费模式(按席位、固定费率、按用量计费)。PITFALLS.md:常见故障模式,包括缺少NEXTAUTH_SECRET、Webhook 密钥不匹配、Edge 运行时与 Drizzle 的冲突、未扩展的 session 类型,以及开发环境与生产环境在迁移策略上的差异。BEST_PRACTICES.md:Stripe 单例模式、使用 Server Actions 处理表单变更、幂等的 Webhook 处理器、为异步仪表盘数据设置Suspense边界、通过stripeCurrentPeriodEnd实现服务端功能开关,以及使用 Upstash Redis 与@upstash/ratelimit对认证路由进行速率限制。