PromptShop

Saas Scaffolder

STRIPE_WEBHOOK_SECRET=whsec_...

Install

npx promptshop add saas-scaffolder

Details

What This Skill Does

The Saa S Scaffolder skill is a powerful tool for product teams that bootstraps full-stack Saa S applications. It generates a complete project scaffold based on user-defined specifications for authentication, database, payments, and features, accelerating development and providing a solid foundation for new Saa S products.

When to Use

  • Quickly scaffold a new Saa S application.
  • Generate a project with Next Auth authentication.
  • Set up a project with Stripe payments.
  • Create a project using Neon DB database.
  • Include specific features in the scaffolded project.
  • Generate a project with a pre-configured UI.

Key Features

Generates a complete file tree for a Saa S application. Supports various authentication providers (Next Auth, Clerk, Supabase). Supports multiple database options (Neon DB, Supabase, Planet Scale). Supports Stripe and Lemon Squeezy payments integration. Includes pre-built UI components and layouts. Generates API routes for authentication, webhooks, and billing.

Manual Installation

Manual installation## 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 (Next Auth)

// lib/auth.ts import { Next Auth Options } from "next-auth" import Google Provider from "next-auth/providers/google" import { Drizzle Adapter } from "@auth/drizzle-adapter" import { db } from "./db"

export const auth Options: Next Auth Options = { adapter: Drizzle Adapter(db), providers: [ Google Provider({ client Id: process.env. GOOGLE_CLIENT_ID!, client Secret: process.env. GOOGLE_CLIENT_SECRET!, }), ], callbacks: { session: async ({ session, user }) => ({ ...session, user: { ...session.user, id: user.id, subscription Status: user.subscription Status, }, }), }, pages: { sign In: "/login" }, }

Database Schema (Drizzle + Neon DB)

// db/schema.ts import { pg Table, text, timestamp, integer } from "drizzle-orm/pg-core"

export const users = pg Table("users", { id: text("id").primary Key().$default Fn(() => crypto.random UUID()), name: text("name"), email: text("email").not Null().unique(), email Verified: timestamp("email Verified"), image: text("image"), stripe Customer Id: text("stripe_customer_id").unique(), stripe Subscription Id: text("stripe_subscription_id"), stripe Price Id: text("stripe_price_id"), stripe Current Period End: timestamp("stripe_current_period_end"), created At: timestamp("created_at").default Now().not Null(), })

export const accounts = pg Table("accounts", { user Id: text("user_id").not Null().references(() => users.id, { on Delete: "cascade" }), type: text("type").not Null(), provider: text("provider").not Null(), provider Account Id: text("provider_account_id").not Null(), 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 { Next Response } from "next/server" import { get Server Session } from "next-auth" import { auth Options } 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 get Server Session(auth Options) if (!session?.user) return Next Response.json({ error: "Unauthorized" }, { status: 401 })

const { price Id } = await req.json() const [user] = await db.select().from(users).where(eq(users.id, session.user.id))

let customer Id = user.stripe Customer Id if (!customer Id) { const customer = await stripe.customers.create({ email: session.user.email! }) customer Id = customer.id await db.update(users).set({ stripe Customer Id: customer Id }).where(eq(users.id, user.id)) }

const checkout Session = await stripe.checkout.sessions.create({ customer: customer Id, mode: "subscription", payment_method_types: ["card"], line_items: [{ price: price Id, 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 Next Response.json({ url: checkout Session.url }) }

Middleware

// middleware.ts import { with Auth } from "next-auth/middleware" import { Next Response } from "next/server"

export default with Auth( function middleware(req) { const token = req.nextauth.token if (req.next Url.pathname.starts With("/dashboard") && !token) { return Next Response.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: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 Type Script 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 Type Script 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 Neon DB/Supabase. Check that the migration has been applied with drizzle-kit push (dev) or drizzle-kit migrate (prod).

Phase 3 — Authentication

[ ] 11. Auth pr