saas-scaffolder
Original:🇺🇸 English
Translated
1 scriptsChecked / no sensitive code detected
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.
7installs
Added on
NPX Install
npx skill4agent add alirezarezvani/claude-skills saas-scaffolderTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →SaaS Scaffolder
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]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.tsKey Component Patterns
Auth Config (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" },
}Database 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"),
})Stripe Checkout Route
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*"],
}Environment Variables Template
bash
# .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_...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. 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.jsonPhase 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 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 migratePhase 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 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"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 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_SECRETPhase 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 for a final production build check. Navigate all routes manually and confirm no broken layouts, missing session data, or hydration errors.
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