SaaS Launch Prompt Pack
What This Pack Solves
Wiring auth, billing, and email together is where most SaaS MVPs stall or ship broken. The integrations look simple but have specific failure modes: Stripe webhooks that don't verify correctly, subscription state that drifts from the database, Clerk middleware that blocks API routes, email that silently fails.
These prompts give you the exact implementation patterns for each integration — tested against production failures, not just happy paths.
When To Use
- You're building a SaaS with Clerk + Stripe + Resend on Next.js
- Stripe webhook handler is failing signature verification
- Subscription status in your database is out of sync with Stripe
- You need to gate features based on Pro/Free plan
- You want correct Clerk middleware that doesn't break API routes
Prompts
Prompt 1: Project Context Block
Paste this at the start of every auth/billing conversation.
I'm building a SaaS with Next.js 15 App Router, TypeScript, Clerk for auth,
Drizzle ORM with Neon PostgreSQL, Stripe for subscriptions, and Resend for email.
Key setup:
- Clerk: auth() in server components/actions, useAuth() in client components
- Database: users table has clerkUserId (text, unique), subscriptionStatus, stripeCustomerId
- Stripe: subscription-based billing, webhooks sent to /api/webhooks/stripe
- Resend: transactional only (no marketing), from 'hello@yourdomain.com'
Do not:
- Use cookies for auth — Clerk handles sessions
Prompt 2: Clerk Middleware Setup
I need a Clerk middleware.ts that correctly protects my app routes.
Requirements:
- Protect all routes under /dashboard and /advisor by default
- Allow public access to: /, /sign-in, /sign-up, /pricing, /api/webhooks/stripe
- API routes under /api/advisor/* require auth (return 401 JSON, not redirect)
- The middleware must NOT interfere with Stripe webhooks — they use raw body
- Use clerkMiddleware() with createRouteMatcher for the public routes list
Show the complete middleware.ts file including the matcher config.Expected output: Complete middleware.ts with correct public route list and API route handling.
Prompt 3: Sync Clerk User to Database on Sign-Up
I need a Clerk webhook handler that creates a user record in my database when
someone signs up.
Requirements:
- Route: POST /api/webhooks/clerk
- Verify the webhook signature using svix (Clerk uses svix for webhook delivery)
- Handle the 'user.created' event
- Create a row in the users table: clerkUserId, email (primary email), createdAt
- Handle 'user.deleted' event — soft delete or remove the user record
- Return 200 on success, 400 on verification failure
My users table schema:
[paste your users table definition]Expected output: Clerk webhook handler with svix verification and user creation logic.
Prompt 4: Stripe Checkout Session
I need a Server Action (or API route) that creates a Stripe Checkout session
for a Pro subscription.
Requirements:
- Get the current user via auth() from @clerk/nextjs/server
- Look up or create a Stripe customer using the user's clerkUserId
- Store stripeCustomerId in the users table on first creation
- Create a Checkout session with:
- mode: 'subscription'
- priceId: process.env.STRIPE_PRO_PRICE_ID
- success_url and cancel_url pointing back to /pricing
- customer: the Stripe customer ID
- client_reference_id: the Clerk userId (for webhook lookup)
- Return the session URL for redirect
Handle the case where the user already has an active subscription.Expected output: Complete checkout session creator with customer lookup/creation and existing subscription guard.
Prompt 5: Stripe Webhook Handler
I need a Stripe webhook handler at /api/webhooks/stripe that keeps my
database subscription status in sync.
Requirements:
- Verify signature with stripe.webhooks.constructEvent() using the RAW request body
(not JSON.parse — Stripe requires the raw bytes for signature verification)
- Handle these events:
- checkout.session.completed → set subscriptionStatus = 'pro', store stripeSubscriptionId
- customer.subscription.updated → update status based on subscription.status
- customer.subscription.deleted → set subscriptionStatus = 'free'
- Look up the user by stripeCustomerId OR client_reference_id (Clerk userId)
- Make all handlers idempotent — safe to call twice with the same event
- Return 200 immediately, do DB work synchronously (this is a webhook — be fast)
My users table has: clerkUserId, stripeCustomerId, stripeSubscriptionId, subscriptionStatusExpected output: Complete webhook handler with raw body parsing, signature verification, and all three event handlers.
Prompt 6: Pro Feature Gating Middleware
I need a utility function and middleware that gates Pro features.
Requirements:
- Server-side function: getUserSubscriptionStatus(clerkUserId) → 'free' | 'pro'
Queries the users table, returns status. Cache with React's cache() for the request.
- isPro(clerkUserId) → boolean helper built on top of it
- Middleware helper for API routes: if not Pro, return 401 JSON { error: 'pro_required' }
- Client-side: useSubscriptionStatus() hook that reads from a /api/user/status endpoint
Show how to use this in:
1. A Server Action that should only run for Pro users
2. An API route that requires Pro
3. A client component that shows/hides a Pro featureExpected output: Server utility, API middleware, and client hook with usage examples for all three contexts.
Prompt 7: Resend Welcome Email on Sign-Up
I need to send a welcome email via Resend when a user signs up.
Requirements:
- Triggered from the Clerk 'user.created' webhook handler (after creating the DB record)
- Use React Email for the template — create a WelcomeEmail component
- Email content:
- Subject: "Welcome to [AppName] — your stack advisor is ready"
- Body: user's first name, one-sentence product description, CTA button to /advisor
- Plain text fallback
- Use fire-and-forget — don't await the Resend call in the webhook response path
- Handle Resend errors silently (log, don't throw — don't fail the webhook for email)
My Resend setup: RESEND_API_KEY env var, sending from 'hello@yourdomain.com'Expected output: React Email WelcomeEmail component + Resend send call wired into the Clerk webhook.
Prompt 8: Billing Portal and Upgrade CTA
I need two things:
1. A "Manage Billing" button that redirects Pro users to the Stripe billing portal
- Server Action that calls stripe.billingPortal.sessions.create()
- Uses the user's stripeCustomerId from the database
- return_url points back to /settings or /pricing
- Redirect to the portal URL
2. An "Upgrade to Pro" CTA component for free users
- Client component that calls the checkout Server Action from Prompt 4
- Shows loading state during redirect
- Displays current plan and limit usage (e.g. "2 / 3 recommendations used")
- Place it in: [describe where — dashboard sidebar, pricing page, result page]
Keep both flows separate — portal for existing customers, checkout for new ones.Expected output: Billing portal Server Action + upgrade CTA component with loading state.
Tuning Notes
- Raw body for Stripe — Always say "raw request body, not JSON.parse." This is the #1 Stripe webhook mistake. The signature verification will fail with a parsed body.
- Idempotent webhook handlers — Stripe retries failed webhooks. Say "make this idempotent" for every event handler.
- Clerk userId vs database id — Keep both. Store
clerkUserIdas a text field. Don't use Clerk's userId as your primary key. - Fire-and-forget email — Don't await Resend in the critical path. A failed email shouldn't fail a sign-up.
Common Failure Modes
- Stripe signature verification fails — You're passing
req.json()instead of the raw body. Useawait req.text()and pass that toconstructEvent(). - Subscription status out of sync — You're only handling
checkout.session.completedbut notcustomer.subscription.updated. Handle all three events. - Clerk middleware blocks API routes —
clerkMiddleware()redirects unauthenticated requests to sign-in. API routes should return 401 JSON, not redirects. UsecreateRouteMatcherto differentiate. - Multiple Stripe customers per user — You're creating a new customer on every checkout instead of looking up the existing one. Store
stripeCustomerIdon first checkout and reuse it.