Syntic

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.

ProductFree Safe

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.

SKILL.md
---
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. 1. Create a dedicated folder for this skill in your local skills library.
  2. 2. Place SKILL.md into that folder.
  3. 3. Restart Syntic and invoke this skill on matching tasks.

Syntic Code (CLI)

  1. 1. Save SKILL.md in your local Syntic Code skills directory.
  2. 2. Keep related files in the same skill folder.
  3. 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 Link
Product

Related Skills