Saas mvp launcher
Koleksi 20 Claude Skills siap pakai untuk pengembangan SaaS, web modern, dan praktik rekayasa perangkat lunak tingkat lanjut.
npx -y skills add roedyrustam/claudevibeskills --skill saas-mvp-launcherAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Roadmap and technical design for bootstrapping high-performance multi-tenant B2B/B2C SaaS MVPs. Use whenever the user wants to start a new SaaS product, plan architecture, choose a tech stack, or needs a launch checklist. Trigger on phrases like "build a SaaS", "SaaS MVP", "multi-tenant app", "launch a product", "Next.js SaaS boilerplate", or any product that needs auth, billing, and a dashboard. Covers Next.js 15, Tailwind CSS v4, Drizzle/Prisma, Stripe, Clerk, and Vercel AI SDK.
SKILL.md
10.6 KB, as published. Nobody here has run it
SaaS MVP Launcher
Opinionated roadmap for launching a production-ready SaaS in 4–8 weeks.
The Stack
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 15 (App Router) | RSC, Server Actions, PPR |
| Styling | Tailwind CSS v4 | CSS-first, zero-config |
| Database | PostgreSQL + Drizzle ORM | Type-safe, fast migrations |
| Auth | Clerk | Passkeys, MFA, org management out of box |
| Payments | Stripe | Subscriptions, invoicing, webhooks |
| AI features | Vercel AI SDK + Claude | Streaming, tool use |
| Cache | Upstash Redis | Serverless-safe |
| File storage | Vercel Blob or Cloudflare R2 | |
| Resend + React Email | Developer-friendly | |
| Monitoring | Sentry + Vercel Analytics | |
| Deployment | Vercel | Zero-config, edge network |
Phase 0 — Foundations (Day 1–2)
Project Bootstrap
npx create-next-app@latest my-saas \
--typescript --tailwind --app --src-dir --turbopack
cd my-saas
# Core dependencies
pnpm add \
@clerk/nextjs \
drizzle-orm \
drizzle-kit \
postgres \
stripe \
@stripe/stripe-js \
ai \
@ai-sdk/anthropic \
resend \
react-email \
@upstash/redis \
@upstash/ratelimit \
zod \
class-variance-authority \
clsx \
tailwind-merge \
lucide-react \
@radix-ui/react-dialog \
@radix-ui/react-dropdown-menu
Environment Variables
# .env.local
# Database
DATABASE_URL=postgresql://...
# Auth (Clerk)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
# Payments (Stripe)
STRIPE_SECRET_KEY=sk_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_...
# AI
ANTHROPIC_API_KEY=sk-ant-...
# Email
RESEND_API_KEY=re_...
# Redis
UPSTASH_REDIS_REST_URL=https://...
UPSTASH_REDIS_REST_TOKEN=...
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
Phase 1 — Auth + Onboarding (Day 3–5)
Clerk Middleware
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"
const isPublicRoute = createRouteMatcher([
"/",
"/sign-in(.*)",
"/sign-up(.*)",
"/api/webhooks(.*)",
"/pricing",
])
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) await auth.protect()
})
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
}
App Structure
app/
├── (marketing)/ # Public pages
│ ├── page.tsx # Landing
│ ├── pricing/page.tsx
│ └── layout.tsx
├── (auth)/ # Clerk auth pages
│ ├── sign-in/[[...sign-in]]/page.tsx
│ └── sign-up/[[...sign-up]]/page.tsx
├── onboarding/ # Post-signup flow
│ └── page.tsx
├── (dashboard)/ # Protected app
│ ├── layout.tsx # Sidebar + auth check
│ ├── dashboard/page.tsx
│ ├── settings/page.tsx
│ └── billing/page.tsx
└── api/
├── webhooks/
│ ├── clerk/route.ts # User sync
│ └── stripe/route.ts # Billing events
└── ai/chat/route.ts # AI streaming
Phase 2 — Database Schema (Day 4–6)
// lib/db/schema.ts
import {
pgTable, text, timestamp, uuid, boolean,
integer, pgEnum, index, uniqueIndex
} from "drizzle-orm/pg-core"
export const planEnum = pgEnum("plan", ["free", "pro", "enterprise"])
export const statusEnum = pgEnum("status", ["active", "canceled", "past_due"])
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
clerkId: text("clerk_id").notNull().unique(),
email: text("email").notNull().unique(),
name: text("name").notNull(),
avatarUrl: text("avatar_url"),
plan: planEnum("plan").notNull().default("free"),
stripeCustomerId: text("stripe_customer_id"),
stripeSubscriptionId: text("stripe_subscription_id"),
subscriptionStatus: statusEnum("subscription_status"),
trialEndsAt: timestamp("trial_ends_at"),
onboardingCompleted: boolean("onboarding_completed").notNull().default(false),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
}, (t) => ({
clerkIdIdx: uniqueIndex("users_clerk_id_idx").on(t.clerkId),
}))
export const workspaces = pgTable("workspaces", {
id: uuid("id").primaryKey().defaultRandom(),
ownerId: uuid("owner_id").notNull().references(() => users.id),
name: text("name").notNull(),
slug: text("slug").notNull().unique(),
plan: planEnum("plan").notNull().default("free"),
createdAt: timestamp("created_at").notNull().defaultNow(),
})
export const workspaceMembers = pgTable("workspace_members", {
workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id),
userId: uuid("user_id").notNull().references(() => users.id),
role: text("role").notNull().default("member"), // owner | admin | member
createdAt: timestamp("created_at").notNull().defaultNow(),
}, (t) => ({
pk: uniqueIndex("wm_pk").on(t.workspaceId, t.userId),
}))
Phase 3 — Stripe Billing (Day 7–10)
Products & Prices Setup (run once)
// scripts/setup-stripe.ts
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const product = await stripe.products.create({
name: "Pro Plan",
description: "Everything you need to grow",
})
const monthlyPrice = await stripe.prices.create({
product: product.id,
currency: "usd",
unit_amount: 1900, // $19.00
recurring: { interval: "month" },
nickname: "Pro Monthly",
})
console.log("Price ID:", monthlyPrice.id)
// Save this in your env: STRIPE_PRO_MONTHLY_PRICE_ID=price_xxx
Checkout Session
// app/actions/billing.actions.ts
"use server"
import Stripe from "stripe"
import { auth } from "@clerk/nextjs/server"
import { redirect } from "next/navigation"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function createCheckoutSession(priceId: string) {
const { userId } = await auth()
if (!userId) redirect("/sign-in")
const user = await getUserByClerkId(userId)
const session = await stripe.checkout.sessions.create({
customer: user.stripeCustomerId ?? undefined,
customer_email: user.stripeCustomerId ? undefined : user.email,
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing`,
metadata: { userId: user.id },
subscription_data: {
trial_period_days: 14,
},
})
redirect(session.url!)
}
Stripe Webhook Handler
// app/api/webhooks/stripe/route.ts
import Stripe from "stripe"
import { NextRequest } from "next/server"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(req: NextRequest) {
const body = await req.text()
const sig = req.headers.get("stripe-signature")!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
} catch {
return new Response("Invalid signature", { status: 400 })
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.CheckoutSession
await handleCheckoutComplete(session)
break
}
case "customer.subscription.updated": {
const sub = event.data.object as Stripe.Subscription
await handleSubscriptionUpdate(sub)
break
}
case "customer.subscription.deleted": {
const sub = event.data.object as Stripe.Subscription
await handleSubscriptionCancel(sub)
break
}
}
return new Response("OK")
}
Phase 4 — AI Features (Day 11–13)
Streaming Chat Route
// app/api/ai/chat/route.ts
import { streamText } from "ai"
import { anthropic } from "@ai-sdk/anthropic"
import { auth } from "@clerk/nextjs/server"
export const maxDuration = 30
export async function POST(req: Request) {
const { userId } = await auth()
if (!userId) return new Response("Unauthorized", { status: 401 })
const { messages } = await req.json()
const result = await streamText({
model: anthropic("claude-sonnet-4-6"),
system: "You are a helpful assistant for our SaaS product.",
messages,
maxTokens: 2000,
onFinish: async ({ usage }) => {
// Track usage per user
await trackAiUsage(userId, usage.totalTokens)
},
})
return result.toDataStreamResponse()
}
Phase 5 — Launch Checklist
Technical
- All env vars in Vercel dashboard
- Stripe webhook endpoint registered (CLI:
stripe listen --forward-to localhost:3000/api/webhooks/stripe) - Clerk webhook for user sync (
user.created,user.deleted) - Database migrations run (
bun drizzle-kit migrate) - Rate limiting on all public APIs
- Error tracking (Sentry DSN configured)
-
next.config.ts— security headers, CSP -
robots.txtandsitemap.xml - OG images for all pages
Business
- Privacy policy + Terms of service
- Cookie consent (if EU users)
- Pricing page with feature comparison
- Trial period defined (14 days recommended)
- Customer support channel (email or Intercom)
- Analytics events for key actions (signup, upgrade, feature use)
Key Rules
- Clerk handles auth — don't roll your own JWT system
- Sync Clerk users to DB via webhook (
user.created) on first sign-up - Never trust client-side plan checks — always verify from DB on server
- Stripe webhook is the source of truth for subscription status — not the checkout session
- Trial on signup — reduces friction; 14 days is standard
- Rate limit AI endpoints per user to control costs
- Server Actions for mutations — not API routes unless external services need them
- Drizzle migrations in CI — never run in app startup
- Feature flags via DB — not env vars — for plan-based feature gating
- Monitor AI token usage per user from day one
Gives 0 of the 12 instructions most databases sql skills give
Counted across 589 of the 662 authors here whose files we hold, read 2026-08-06
- use parameterized queriesin 36 of 589, across 32 files
- use timestamptz for timestampsin 30 of 589, across 12 files
- create indexes concurrentlyin 29 of 589, across 23 files
- index foreign keysin 28 of 589, across 17 files
- use numeric type for moneyin 25 of 589, across 8 files
- select only required columnsin 24 of 589, across 19 files
- use cursor pagination instead of OFFSETin 23 of 589, across 15 files
- add indexes manually on foreign key columnsin 22 of 589, across 11 files
- read individual rule files for detailed explanationsin 18 of 589, across 4 files
- configure connection poolingin 18 of 589, across 16 files
- put equality columns before range columns in indexesin 17 of 589, across 9 files
- normalize to third normal formin 17 of 589, across 8 files
Said here and by no other author read
- scaffold the project using create-next-app
- install core SaaS dependencies
- secure routes using clerk middleware
- define users and workspaces database schema
- create stripe billing checkout sessions
- handle stripe subscription webhook events
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.