SAAS scaffolder

Generates complete, production-ready SaaS project boilerplate including authentication, database schemas, billing integration, API routes, and a working dashboard using Next.js 14+ App Router, TypeScript, Tailwind CSS, shadcn/ui, Drizzle ORM, and Stripe.

How to use it

Claude Code
  1. Run the line below. It pulls the whole folder into ~/.claude/skills/saas-scaffolder, including the files SKILL.md points to.
  2. Describe your job in plain words. Claude Code follows the skill from there.
Claude Code — installs the whole folder, not just SKILL.md
npx degit alirezarezvani/claude-skills/product-team/skills/saas-scaffolder#main ~/.claude/skills/saas-scaffolder

For one project only, change the path to .claude/skills/saas-scaffolder. This skill also uses Next.js, auth.ts, db.ts, stripe.ts, validations.ts, utils.ts — copying SKILL.md alone won't be enough. See the folder on GitHub.

Claude (web or desktop app)
  1. On this page open ⋯ → Download .md.
  2. Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
  3. Pick the file and Save. Claude shows the name and description and runs a security scan.
  4. Check the skill is switched on.
  5. Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
  1. ChatGPT: make a Project and paste it into Instructions.
  2. Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
  • Check which app you pasted it into — the steps above name the right one.
  • Some skills need the paid tier of Claude or ChatGPT.
Step-by-step guide with screenshots · Ask in the forum

Paste into Claude, ChatGPT or Cursor.

Source of SAAS scaffolder

Show the full text294 lines
namedescription
saas-scaffolderGenerates complete, production-ready SaaS project boilerplate including authentication, database schemas, billing integration, API routes, and a working dashboard using Next.js 14+ App Router, TypeScript, Tailwind CSS, shadcn/ui, Drizzle ORM, and Stripe. Use when the user wants to create a new SaaS app, start a subscription-based web project, scaffold a Next.js application, or mentions terms like starter template, boilerplate, new project, or wiring up auth and payments.

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

The following phases must be completed in order. Validate at the end of each phase before proceeding.

Phase 1 — Foundation
  • 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.

Phase 2 — Database
  • 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).

Phase 3 — Authentication
  • 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.

Phase 4 — Payments
  • 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.

Phase 5 — UI
  • 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.


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.
1---
2name: "saas-scaffolder"
3description: "Generates complete, production-ready SaaS project boilerplate including authentication, database schemas, billing integration, API routes, and a working dashboard using Next.js 14+ App Router, TypeScript, Tailwind CSS, shadcn/ui, Drizzle ORM, and Stripe. Use when the user wants to create a new SaaS app, start a subscription-based web project, scaffold a Next.js application, or mentions terms like starter template, boilerplate, new project, or wiring up auth and payments."
4---
5 
6# SaaS Scaffolder
7 
8**Tier:** POWERFUL
9**Category:** Product Team
10**Domain:** Full-Stack Development / Project Bootstrapping
11 
12---
13 
14## Input Format
15 
16```
17Product: [name]
18Description: [1-3 sentences]
19Auth: nextauth | clerk | supabase
20Database: neondb | supabase | planetscale
21Payments: stripe | lemonsqueezy | none
22Features: [comma-separated list]
23```
24 
25---
26 
27## File Tree Output
28 
29```
30my-saas/
31├── app/
32│ ├── (auth)/
33│ │ ├── login/page.tsx
34│ │ ├── register/page.tsx
35│ │ └── layout.tsx
36│ ├── (dashboard)/
37│ │ ├── dashboard/page.tsx
38│ │ ├── settings/page.tsx
39│ │ ├── billing/page.tsx
40│ │ └── layout.tsx
41│ ├── (marketing)/
42│ │ ├── page.tsx
43│ │ ├── pricing/page.tsx
44│ │ └── layout.tsx
45│ ├── api/
46│ │ ├── auth/[...nextauth]/route.ts
47│ │ ├── webhooks/stripe/route.ts
48│ │ ├── billing/checkout/route.ts
49│ │ └── billing/portal/route.ts
50│ └── layout.tsx
51├── components/
52│ ├── ui/
53│ ├── auth/
54│ │ ├── login-form.tsx
55│ │ └── register-form.tsx
56│ ├── dashboard/
57│ │ ├── sidebar.tsx
58│ │ ├── header.tsx
59│ │ └── stats-card.tsx
60│ ├── marketing/
61│ │ ├── hero.tsx
62│ │ ├── features.tsx
63│ │ ├── pricing.tsx
64│ │ └── footer.tsx
65│ └── billing/
66│ ├── plan-card.tsx
67│ └── usage-meter.tsx
68├── lib/
69│ ├── auth.ts
70│ ├── db.ts
71│ ├── stripe.ts
72│ ├── validations.ts
73│ └── utils.ts
74├── db/
75│ ├── schema.ts
76│ └── migrations/
77├── hooks/
78│ ├── use-subscription.ts
79│ └── use-user.ts
80├── types/index.ts
81├── middleware.ts
82├── .env.example
83├── drizzle.config.ts
84└── next.config.ts
85```
86 
87---
88 
89## Key Component Patterns
90 
91### Auth Config (NextAuth)
92 
93```typescript
94// lib/auth.ts
95import { NextAuthOptions } from "next-auth"
96import GoogleProvider from "next-auth/providers/google"
97import { DrizzleAdapter } from "@auth/drizzle-adapter"
98import { db } from "./db"
99 
100export const authOptions: NextAuthOptions = {
101 adapter: DrizzleAdapter(db),
102 providers: [
103 GoogleProvider({
104 clientId: process.env.GOOGLE_CLIENT_ID!,
105 clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
106 }),
107 ],
108 callbacks: {
109 session: async ({ session, user }) => ({
110 ...session,
111 user: {
112 ...session.user,
113 id: user.id,
114 subscriptionStatus: user.subscriptionStatus,
115 },
116 }),
117 },
118 pages: { signIn: "/login" },
119}
120```
121 
122### Database Schema (Drizzle + NeonDB)
123 
124```typescript
125// db/schema.ts
126import { pgTable, text, timestamp, integer } from "drizzle-orm/pg-core"
127 
128export const users = pgTable("users", {
129 id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
130 name: text("name"),
131 email: text("email").notNull().unique(),
132 emailVerified: timestamp("emailVerified"),
133 image: text("image"),
134 stripeCustomerId: text("stripe_customer_id").unique(),
135 stripeSubscriptionId: text("stripe_subscription_id"),
136 stripePriceId: text("stripe_price_id"),
137 stripeCurrentPeriodEnd: timestamp("stripe_current_period_end"),
138 createdAt: timestamp("created_at").defaultNow().notNull(),
139})
140 
141export const accounts = pgTable("accounts", {
142 userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
143 type: text("type").notNull(),
144 provider: text("provider").notNull(),
145 providerAccountId: text("provider_account_id").notNull(),
146 refresh_token: text("refresh_token"),
147 access_token: text("access_token"),
148 expires_at: integer("expires_at"),
149})
150```
151 
152### Stripe Checkout Route
153 
154```typescript
155// app/api/billing/checkout/route.ts
156import { NextResponse } from "next/server"
157import { getServerSession } from "next-auth"
158import { authOptions } from "@/lib/auth"
159import { stripe } from "@/lib/stripe"
160import { db } from "@/lib/db"
161import { users } from "@/db/schema"
162import { eq } from "drizzle-orm"
163 
164export async function POST(req: Request) {
165 const session = await getServerSession(authOptions)
166 if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
167 
168 const { priceId } = await req.json()
169 const [user] = await db.select().from(users).where(eq(users.id, session.user.id))
170 
171 let customerId = user.stripeCustomerId
172 if (!customerId) {
173 const customer = await stripe.customers.create({ email: session.user.email! })
174 customerId = customer.id
175 await db.update(users).set({ stripeCustomerId: customerId }).where(eq(users.id, user.id))
176 }
177 
178 const checkoutSession = await stripe.checkout.sessions.create({
179 customer: customerId,
180 mode: "subscription",
181 payment_method_types: ["card"],
182 line_items: [{ price: priceId, quantity: 1 }],
183 success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?upgraded=true`,
184 cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
185 subscription_data: { trial_period_days: 14 },
186 })
187 
188 return NextResponse.json({ url: checkoutSession.url })
189}
190```
191 
192### Middleware
193 
194```typescript
195// middleware.ts
196import { withAuth } from "next-auth/middleware"
197import { NextResponse } from "next/server"
198 
199export default withAuth(
200 function middleware(req) {
201 const token = req.nextauth.token
202 if (req.nextUrl.pathname.startsWith("/dashboard") && !token) {
203 return NextResponse.redirect(new URL("/login", req.url))
204 }
205 },
206 { callbacks: { authorized: ({ token }) => !!token } }
207)
208 
209export const config = {
210 matcher: ["/dashboard/:path*", "/settings/:path*", "/billing/:path*"],
211}
212```
213 
214### Environment Variables Template
215 
216```bash
217# .env.example
218NEXT_PUBLIC_APP_URL=http://localhost:3000
219DATABASE_URL=postgresql://user:[email protected]/neondb?sslmode=require
220NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32
221NEXTAUTH_URL=http://localhost:3000
222GOOGLE_CLIENT_ID=
223GOOGLE_CLIENT_SECRET=
224STRIPE_SECRET_KEY=sk_test_...
225STRIPE_WEBHOOK_SECRET=whsec_...
226NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
227STRIPE_PRO_PRICE_ID=price_...
228```
229 
230---
231 
232## Scaffold Checklist
233 
234The following phases must be completed in order. **Validate at the end of each phase before proceeding.**
235 
236### Phase 1 — Foundation
237- [ ] 1. Next.js initialized with TypeScript and App Router
238- [ ] 2. Tailwind CSS configured with custom theme tokens
239- [ ] 3. shadcn/ui installed and configured
240- [ ] 4. ESLint + Prettier configured
241- [ ] 5. `.env.example` created with all required variables
242 
243✅ **Validate:** Run `npm run build` — no TypeScript or lint errors should appear.
244🔧 **If build fails:** Check `tsconfig.json` paths and that all shadcn/ui peer dependencies are installed.
245 
246### Phase 2 — Database
247- [ ] 6. Drizzle ORM installed and configured
248- [ ] 7. Schema written (users, accounts, sessions, verification_tokens)
249- [ ] 8. Initial migration generated and applied
250- [ ] 9. DB client singleton exported from `lib/db.ts`
251- [ ] 10. DB connection tested in local environment
252 
253✅ **Validate:** Run a simple `db.select().from(users)` in a test script — it should return an empty array without throwing.
254🔧 **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).
255 
256### Phase 3 — Authentication
257- [ ] 11. Auth provider installed (NextAuth / Clerk / Supabase)
258- [ ] 12. OAuth provider configured (Google / GitHub)
259- [ ] 13. Auth API route created
260- [ ] 14. Session callback adds user ID and subscription status
261- [ ] 15. Middleware protects dashboard routes
262- [ ] 16. Login and register pages built with error states
263 
264✅ **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`.
265🔧 **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.
266 
267### Phase 4 — Payments
268- [ ] 17. Stripe client initialized with TypeScript types
269- [ ] 18. Checkout session route created
270- [ ] 19. Customer portal route created
271- [ ] 20. Stripe webhook handler with signature verification
272- [ ] 21. Webhook updates user subscription status in DB idempotently
273 
274✅ **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).
275🔧 **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.
276 
277### Phase 5 — UI
278- [ ] 22. Landing page with hero, features, pricing sections
279- [ ] 23. Dashboard layout with sidebar and responsive header
280- [ ] 24. Billing page showing current plan and upgrade options
281- [ ] 25. Settings page with profile update form and success states
282 
283✅ **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.
284 
285---
286 
287## Reference Files
288 
289For additional guidance, generate the following companion reference files alongside the scaffold:
290 
291- **`CUSTOMIZATION.md`** — Auth providers, database options, ORM alternatives, payment providers, UI themes, and billing models (per-seat, flat-rate, usage-based).
292- **`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.
293- **`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`.
294 

Discussion

Alternatives

Also in Web frameworksSee all 533 in Development →