Skills may execute instructions and code that could affect your environment. Marketplace scans reduce risk but do not guarantee safety. Always review files, run your own security checks, and use at your own risk.
saas-scaffolder
Security Scan Summary
Status: Safe
Source: Syntic Skills registry
Automated security scan completed with no high-risk patterns detected. Manual review is still required.
About This Skill
Use when the user wants to create a new SaaS app, scaffold a Next.js project with auth/database/billing, or asks for a subscription-app starter template or boilerplate.
Downloadable SKILL.md
Download SKILL.md and place it in your Syntic skills folder. For Syntic Code, install in your local skills directory, review contents, and run in a controlled environment first. Acknowledge the risk notice above to enable the download.
---
name: saas-scaffolder
description: Use when the user wants to create a new SaaS app, scaffold a Next.js project with auth/database/billing, or asks for a subscription-app starter template or boilerplate.
category: Product
version: 1.0.0
tools: []
---
# SaaS Scaffolder
**Tier:** POWERFUL
**Category:** Product Team
**Domain:** Full-Stack Development / Project Bootstrapping
Produce every file below as a labeled markdown code block in chat, one phase at a time — Amara has no filesystem, so the user copies each block into their own project.
---
## 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)
```typescript
// lib/auth.ts
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" },
}
```
Imports: `NextAuthOptions` from `next-auth`, `GoogleProvider` from `next-auth/providers/google`, `DrizzleAdapter` from `@auth/drizzle-adapter`, and the local `db` client.
### 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(),
access_token: text("access_token"),
})
```
### Stripe Checkout Route
```typescript
// app/api/billing/checkout/route.ts
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",
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 })
}
```
Imports: `NextResponse` from `next/server`, `getServerSession` + `authOptions` for the session, `stripe` client, `db`/`users` schema, and `eq` from `drizzle-orm`.
### Middleware
`middleware.ts` wraps `withAuth` from `next-auth/middleware`: redirect to `/login` when `req.nextauth.token` is missing on a protected path, and set `matcher: ["/dashboard/:path*", "/settings/:path*", "/billing/:path*"]` so it only runs where needed.
### 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
Walk the user through five phases in order, presenting each as chat sections with the code for that phase. Tell the user what to verify locally before moving to the next phase.
| Phase | Deliverables | User verifies | Common failure & fix |
|---|---|---|---|
| **1. Foundation** | Next.js + TypeScript + App Router; Tailwind with theme tokens; shadcn/ui; ESLint + Prettier; `.env.example` | `npm run build` — no TypeScript/lint errors | Build fails → check `tsconfig.json` paths and shadcn/ui peer dependencies |
| **2. Database** | Drizzle ORM; schema (users, accounts, sessions, verification_tokens); initial migration; `lib/db.ts` client singleton | `db.select().from(users)` returns an empty array without throwing | Connection fails → confirm `DATABASE_URL` includes `?sslmode=require` for NeonDB/Supabase, migration applied via `drizzle-kit push`/`migrate` |
| **3. Authentication** | Auth provider (NextAuth/Clerk/Supabase); OAuth (Google/GitHub); auth API route; session callback adds `id` + `subscriptionStatus`; middleware guards dashboard; login/register pages | Sign in via OAuth, session has `id`/`subscriptionStatus`; unauthenticated `/dashboard` redirects to `/login` | Sign-out loops in prod → `NEXTAUTH_SECRET` set and consistent across deployments; extend session types via `declare module "next-auth"` |
| **4. Payments** | Stripe client; checkout session route; customer portal route; webhook handler with signature verification; idempotent subscription-status updates | Test checkout with `4242 4242 4242 4242`; `stripeSubscriptionId` written to DB; replaying `checkout.session.completed` doesn't duplicate writes | Webhook signature fails → use `stripe listen --forward-to localhost:3000/api/webhooks/stripe` locally; verify `STRIPE_WEBHOOK_SECRET` matches |
| **5. UI** | Landing page (hero/features/pricing); dashboard layout (sidebar, header); billing page; settings page | Final `npm run build`; walk every route for broken layouts, missing session data, hydration errors | — |
## Customization & Pitfalls
When asked, expand on: **auth/DB/payment alternatives** (Clerk vs NextAuth vs Supabase Auth; Neon vs Supabase vs PlanetScale; Stripe vs LemonSqueezy) and billing models (per-seat, flat-rate, usage-based); **common failure modes** (missing `NEXTAUTH_SECRET`, webhook secret mismatches, Edge runtime conflicts with Drizzle, unextended session types, dev/prod migration drift); **best practices** (Stripe client singleton, server actions for form mutations, idempotent webhook handlers, `Suspense` boundaries for async dashboard data, server-side feature gating via `stripeCurrentPeriodEnd`, rate limiting auth routes with Upstash Redis + `@upstash/ratelimit`).
Bundle Download
Includes SKILL.md and bundled support files where provided. Risk acknowledgement is required.
Install Targets
Syntic App
- 1. Create a dedicated folder for this skill in your local skills library.
- 2. Place SKILL.md into that folder.
- 3. Restart Syntic and invoke this skill on matching tasks.
Syntic Code (CLI)
- 1. Save SKILL.md in your local Syntic Code skills directory.
- 2. Keep related files in the same skill folder.
- 3. Run in a safe environment and validate outputs.
Source
https://github.com/alirezarezvani/claude-skills/blob/main/product-team/skills/saas-scaffolder/SKILL.md
Open Source LinkRelated Skills
agile-product-owner
Use when writing user stories, creating acceptance criteria, planning sprints, estimating story points...
Productapple-hig-expert
Use when auditing iOS/macOS/watchOS/visionOS interfaces against HIG, checking accessibility standards, or...
ProductBusiness Analyst
Use when performing requirements analysis, process mapping, gap analysis, and aligning stakeholders on...
Productcode-to-prd
Use when reverse-engineering a frontend, backend, or fullstack codebase into a business-readable Product...