| name | saas-scaffolder |
| description | Generate complete production-ready SaaS boilerplate with authentication, database schemas, billing integration (Stripe), multi-tenancy, API routes, dashboard UI, and deployment configuration. Supports Next.js App Router, TypeScript, Tailwind, shadcn/ui, Drizzle ORM, and multiple auth/payment providers. Use when starting a new SaaS product, subscription app, or multi-tenant platform.
|
| license | MIT + Commons Clause |
| metadata | {"version":"1.0.0","author":"borghei","category":"engineering","domain":"full-stack","tier":"POWERFUL","updated":"2026-03-09T00:00:00.000Z","frameworks":"nextjs, drizzle, stripe, nextauth, tailwind, shadcn"} |
SaaS Scaffolder
Tier: POWERFUL
Category: Engineering / Full-Stack
Maintainer: Claude Skills Team
Overview
Generate a complete, production-ready SaaS application boilerplate including authentication (NextAuth, Clerk, or Supabase Auth), database schemas with multi-tenancy, billing integration (Stripe or Lemon Squeezy), API routes with validation, dashboard UI with shadcn/ui, and deployment configuration. Produces a working application from a product specification in under 30 minutes.
Keywords
SaaS, boilerplate, scaffolding, Next.js, authentication, Stripe, billing, multi-tenancy, subscription, starter template, NextAuth, Drizzle ORM, shadcn/ui
Input Specification
Product: [name]
Description: [1-3 sentences]
Auth: nextauth | clerk | supabase
Database: neondb | supabase | planetscale | turso
Payments: stripe | lemonsqueezy | none
Multi-tenancy: workspace | organization | none
Features: [comma-separated list]
Generated File Tree
my-saas/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ ├── register/page.tsx
│ │ ├── forgot-password/page.tsx
│ │ └── layout.tsx
│ ├── (dashboard)/
│ │ ├── dashboard/page.tsx
│ │ ├── settings/
│ │ │ ├── page.tsx # Profile settings
│ │ │ ├── billing/page.tsx # Subscription management
│ │ │ └── team/page.tsx # Team/workspace settings
│ │ └── layout.tsx # Dashboard shell (sidebar + header)
│ ├── (marketing)/
│ │ ├── page.tsx # Landing page
│ │ ├── pricing/page.tsx # Pricing tiers
│ │ └── layout.tsx
│ ├── api/
│ │ ├── auth/[...nextauth]/route.ts
│ │ ├── webhooks/stripe/route.ts
│ │ ├── billing/
│ │ │ ├── checkout/route.ts
│ │ │ └── portal/route.ts
│ │ └── health/route.ts
│ ├── layout.tsx # Root layout
│ └── not-found.tsx
├── components/
│ ├── ui/ # shadcn/ui components
│ ├── auth/
│ │ ├── login-form.tsx
│ │ └── register-form.tsx
│ ├── dashboard/
│ │ ├── sidebar.tsx
│ │ ├── header.tsx
│ │ └── stats-card.tsx
│ ├── marketing/
│ │ ├── hero.tsx
│ │ ├── features.tsx
│ │ ├── pricing-card.tsx
│ │ └── footer.tsx
│ └── billing/
│ ├── plan-card.tsx
│ └── usage-meter.tsx
├── lib/
│ ├── auth.ts # Auth configuration
│ ├── db.ts # Database client singleton
│ ├── stripe.ts # Stripe client
│ ├── validations.ts # Zod schemas
│ └── utils.ts # Shared utilities
├── db/
│ ├── schema.ts # Drizzle schema
│ ├── migrations/ # Generated migrations
│ └── seed.ts # Development seed data
├── hooks/
│ ├── use-subscription.ts
│ └── use-current-user.ts
├── types/
│ └── index.ts # Shared TypeScript types
├── middleware.ts # Auth + rate limiting
├── .env.example
├── drizzle.config.ts
├── tailwind.config.ts
└── next.config.ts
Database Schema (Multi-Tenant)
import { pgTable, text, timestamp, integer, boolean, uniqueIndex, index } from 'drizzle-orm/pg-core'
import { createId } from '@paralleldrive/cuid2'
export const workspaces = pgTable('workspaces', {
id: text('id').primaryKey().$defaultFn(createId),
name: text('name').notNull(),
slug: text('slug').notNull(),
plan: text('plan').notNull().default('free'),
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', { withTimezone: }).().(),
: (, { : }).().(),
}, [
().(t.),
])
users = (, {
: ().().$defaultFn(createId),
: ().().(),
: (),
: (),
: (, { : }),
: (, { : }).().(),
})
workspaceMembers = (, {
: ().().$defaultFn(createId),
: ().().( workspaces., { : }),
: ().().( users., { : }),
: ().().(),
: (, { : }).().(),
}, [
().(t., t.),
().(t.),
])
accounts = (, {
: ().().( users., { : }),
: ().(),
: ().(),
: ().(),
: (),
: (),
: (),
})
sessions = (, {
: ().(),
: ().().( users., { : }),
: (, { : }).(),
})
Authentication Configuration
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
import GitHub from 'next-auth/providers/github'
import Resend from 'next-auth/providers/resend'
import { db } from './db'
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: DrizzleAdapter(db),
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
Resend({
from: 'noreply@myapp.com',
}),
],
callbacks: {
session: async ({ session, user }) => ({
...session,
: {
...session.,
: user.,
},
}),
},
: {
: ,
: ,
},
})
Stripe Billing Integration
Checkout Session
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { stripe } from '@/lib/stripe'
import { db } from '@/lib/db'
import { workspaces } from '@/db/schema'
import { eq } from 'drizzle-orm'
export async function POST(req: Request) {
const session = await auth()
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { priceId, workspaceId } = await req.json()
const [workspace] = await db.select().from(workspaces).where(eq(workspaces.id, workspaceId))
if (!workspace) {
.({ : }, { : })
}
customerId = workspace.
(!customerId) {
customer = stripe..({
: session..!,
: { workspaceId },
})
customerId = customer.
db.(workspaces)
.({ : customerId })
.((workspaces., workspaceId))
}
checkoutSession = stripe...({
: customerId,
: ,
: [],
: [{ : priceId, : }],
: ,
: ,
: { : },
: { workspaceId },
})
.({ : checkoutSession. })
}
Webhook Handler
import { headers } from 'next/headers'
import { stripe } from '@/lib/stripe'
import { db } from '@/lib/db'
import { workspaces } from '@/db/schema'
import { eq } from 'drizzle-orm'
export async function POST(req: Request) {
const body = await req.text()
const signature = (await headers()).get('Stripe-Signature')!
let event
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)
} catch (err) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 })
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.
subscription = stripe..(session. )
db.(workspaces).({
: subscription.,
: subscription..[]..,
: (subscription. * ),
}).((workspaces., session. ))
}
: {
invoice = event..
subscription = stripe..(invoice. )
db.(workspaces).({
: (subscription. * ),
}).((workspaces., invoice. ))
}
: {
subscription = event..
db.(workspaces).({
: ,
: ,
: ,
: ,
}).((workspaces., subscription. ))
}
}
(, { : })
}
Middleware (Auth + Rate Limiting)
import { auth } from '@/lib/auth'
import { NextResponse } from 'next/server'
export default auth((req) => {
const { pathname } = req.nextUrl
const isAuthenticated = !!req.auth
if (pathname.startsWith('/dashboard') || pathname.startsWith('/settings')) {
if (!isAuthenticated) {
return NextResponse.redirect(new URL('/login', req.url))
}
}
if ((pathname === '/login' || pathname === '/register') && isAuthenticated) {
return NextResponse.redirect(new URL('/dashboard', req.url))
}
return NextResponse.next()
})
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', , ],
}
Environment Variables
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
DATABASE_URL=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_PRO_MONTHLY_PRICE_ID=price_...
STRIPE_PRO_YEARLY_PRICE_ID=price_...
RESEND_API_KEY=re_...
SENTRY_DSN=
Scaffolding Phases
Execute these phases in order. Validate at the end of each phase.
Phase 1: Foundation
- Initialize Next.js with TypeScript and App Router
- Configure Tailwind CSS with custom theme
- Install and configure shadcn/ui
- Set up ESLint and Prettier
- Create
.env.example
Validate: pnpm build completes without errors.
Phase 2: Database
- Install and configure Drizzle ORM
- Write schema (users, accounts, sessions, workspaces, members)
- Generate and apply initial migration
- Export DB client singleton from
lib/db.ts
- Create seed script with test data
Validate: pnpm db:push succeeds and pnpm db:seed creates test data.
Phase 3: Authentication
- Install and configure NextAuth v5 with Drizzle adapter
- Set up OAuth providers (Google, GitHub)
- Create auth API route
- Implement middleware for route protection
- Build login and register pages
Validate: OAuth login works, session persists, protected routes redirect.
Phase 4: Billing
- Initialize Stripe client
- Create checkout session API route
- Create customer portal API route
- Implement webhook handler with signature verification
- Build pricing page and billing settings page
Validate: Complete a test checkout with card 4242 4242 4242 4242. Verify subscription data written to DB. Replay webhook event and confirm idempotency.
Phase 5: UI and Polish
- Build landing page (hero, features, pricing, footer)
- Build dashboard layout (sidebar, header, stats)
- Build settings pages (profile, billing, team)
- Add loading states, error boundaries, and not-found pages
- Configure deployment (Vercel/Railway)
Validate: pnpm build succeeds. All routes render correctly. No hydration errors.
Multi-Tenancy Patterns
Workspace-Scoped Queries
export async function getProjects(workspaceId: string) {
return db.query.projects.findMany({
where: eq(projects.workspaceId, workspaceId),
orderBy: [desc(projects.updatedAt)],
})
}
export function getCurrentWorkspace(req: Request) {
}
Plan-Based Feature Gating
export function canAccessFeature(workspace: Workspace, feature: string): boolean {
const PLAN_FEATURES: Record<string, string[]> = {
free: ['basic_dashboard', 'up_to_3_members'],
pro: ['advanced_analytics', 'up_to_20_members', 'custom_domain', 'api_access'],
enterprise: ['sso', 'unlimited_members', 'audit_log', 'sla'],
}
const isActive = workspace.stripeCurrentPeriodEnd
? workspace.stripeCurrentPeriodEnd > new Date()
: workspace.plan === 'free'
if (!isActive) return PLAN_FEATURES.free.includes(feature)
return PLAN_FEATURES[workspace.plan]?.includes(feature) ?? false
}
Common Pitfalls
- Missing
NEXTAUTH_SECRET in production — causes session errors; generate with openssl rand -base64 32
- Webhook signature verification skipped — always verify Stripe webhook signatures; test with
stripe listen
workspace:* in session but not refreshed — stale subscription data; recheck on billing pages
- Edge Runtime conflicts with Drizzle — Drizzle needs Node.js runtime; set
export const runtime = 'nodejs' on API routes
- No idempotent webhook handling — Stripe may send duplicate events; use
event.id for deduplication
- Hardcoded Stripe price IDs — store in env vars, not in code; prices change between test and live mode
Best Practices
- Stripe singleton — create the client once in
lib/stripe.ts, import everywhere
- Server actions for form mutations — use Next.js Server Actions instead of API routes for forms
- Idempotent webhook handlers — check if the event was already processed before writing to DB
- Suspense boundaries for async data — wrap dashboard data in
<Suspense> with loading skeletons
- Feature gating at the server level — check
stripeCurrentPeriodEnd on the server, not the client
- Rate limiting on auth routes — prevent brute force with Upstash Redis +
@upstash/ratelimit
- Workspace context in every query — never query without scoping to the current workspace
- Test with Stripe CLI —
stripe listen --forward-to localhost:3000/api/webhooks/stripe for local development
Troubleshooting
| Problem | Cause | Solution |
|---|
NEXTAUTH_URL mismatch errors in production | Environment variable not updated from localhost default | Set NEXTAUTH_URL to your actual production domain; omit trailing slash |
| Stripe webhook returns 400 on every event | Raw body is consumed before signature verification | Ensure the webhook route uses req.text() before any JSON parsing; do not use body-parser middleware on the webhook endpoint |
| Drizzle migrations fail with "relation already exists" | Migration was partially applied or schema drifted from migration history | Run pnpm drizzle-kit drop to reset the migration journal, then regenerate with pnpm drizzle-kit generate and reapply |
| OAuth callback redirects to wrong URL | Redirect URI registered in provider console does not match NEXTAUTH_URL | Update the authorized redirect URI in Google/GitHub developer console to match your deployment URL exactly |
| Multi-tenant queries return data from other workspaces | Missing workspaceId filter in a database query | Audit all db.query and db.select calls to ensure every query includes a where clause scoped to the current workspace |
| Hydration mismatch on dashboard pages | Server-rendered HTML differs from client due to conditional auth checks | Move auth-dependent rendering into client components or wrap with <Suspense>; avoid reading session in server components that also render on the client |
| Stripe test mode charges succeed but live mode fails | Live mode price IDs differ from test mode IDs | Use separate environment variables for test vs. live Stripe keys and price IDs; verify .env.production references the correct live values |
Success Criteria
- Scaffolded project passes
pnpm build with zero errors and zero TypeScript warnings on first run
- End-to-end authentication flow (register, login, logout, password reset) completes in under 60 seconds of manual testing
- Stripe checkout creates a subscription and webhook handler updates the database within 5 seconds of payment completion
- Multi-tenant data isolation verified: queries scoped to Workspace A return zero rows belonging to Workspace B
- Lighthouse performance score on the landing page is 90+ on mobile with no accessibility violations at the AA level
- Time from
git clone to running local dev server with seeded data is under 10 minutes following the generated README
- All environment variables are documented in
.env.example with descriptions, and the app fails fast with clear error messages when required variables are missing
Scope & Limitations
This skill covers:
- Full-stack SaaS scaffolding with Next.js App Router, TypeScript, Tailwind, and shadcn/ui
- Authentication setup with NextAuth v5, Clerk, or Supabase Auth including OAuth and magic link providers
- Stripe and Lemon Squeezy billing integration with checkout, webhooks, and customer portal
- Multi-tenancy patterns (workspace/organization) with role-based access and plan-based feature gating
This skill does NOT cover:
- Ongoing Stripe billing logic beyond initial integration (metered billing, usage-based pricing, invoicing customization) — see
stripe-integration-expert
- Database schema design decisions beyond the core tenancy model (complex relational modeling, indexing strategies) — see
database-schema-designer
- CI/CD pipeline configuration, deployment automation, or infrastructure provisioning — see
ci-cd-pipeline-builder
- API design standards, versioning, or OpenAPI specification generation — see
api-design-reviewer
Integration Points
| Skill | Integration | Data Flow |
|---|
stripe-integration-expert | Extends the scaffolded Stripe setup with advanced billing patterns (metered, tiered, usage-based) | Scaffolder outputs base Stripe config and webhook handler; Stripe expert refines pricing models and adds invoice customization |
database-schema-designer | Designs extended schemas beyond the core tenancy tables | Scaffolder provides baseline users/workspaces/members schema; schema designer adds domain-specific entities and optimizes indexes |
api-design-reviewer | Reviews and improves the generated API routes for consistency and standards compliance | Scaffolder generates initial API routes; reviewer audits naming, error handling, and response formats |
ci-cd-pipeline-builder | Creates deployment pipelines for the scaffolded project | Scaffolder outputs the application code; pipeline builder adds GitHub Actions, preview deployments, and production release workflows |
env-secrets-manager | Audits and secures the environment variable configuration | Scaffolder generates .env.example; secrets manager validates no secrets are hardcoded and recommends vault integration |
observability-designer | Adds logging, tracing, and monitoring to the scaffolded application | Scaffolder provides the application structure; observability designer instruments API routes, webhooks, and auth flows |