How to Deploy Your SaaS Boilerplate to Production 2026
TL;DR
Vercel + Neon + Resend + Stripe (live mode) is the default production stack for Next.js SaaS in 2026. Deployment itself takes 1-2 hours. The checklist below covers the steps most developers forget — especially environment variables, database migrations, domain setup, and Stripe webhook registration.
The Production Stack
| Layer | Service | Cost |
|---|---|---|
| Hosting | Vercel Pro | $20/mo |
| Database | Neon (Serverless Postgres) | $19/mo |
| Resend | $0 → $20/mo | |
| Payments | Stripe | 2.9% + 30¢ |
| Auth | NextAuth (self-hosted) | Free |
| Error tracking | Sentry | Free tier |
| Analytics | PostHog | Free tier |
Step 1: Database (Neon)
# 1. Create production database at neon.tech
# 2. Copy connection string
# 3. Run migrations against production
DATABASE_URL="postgresql://..." npx prisma migrate deploy
# 4. Verify migrations
DATABASE_URL="postgresql://..." npx prisma db push --preview-feature
Neon branching for zero-downtime migrations:
# Before deploying a breaking migration:
# 1. Create a branch: main → migration-branch
# 2. Apply migration to branch
# 3. Deploy app pointing to branch
# 4. Verify, then promote branch to main
Step 2: Vercel Project Setup
# Install Vercel CLI
npm i -g vercel
# Link and deploy
vercel --prod
# Or connect via GitHub:
# vercel.com → New Project → Import from GitHub
Build settings for Next.js:
// vercel.json (optional — usually auto-detected)
{
"buildCommand": "npm run build",
"outputDirectory": ".next",
"installCommand": "npm ci"
}
If using Prisma, generate client during build:
// package.json
{
"scripts": {
"build": "prisma generate && next build",
"postinstall": "prisma generate"
}
}
Step 3: Environment Variables
Set in Vercel dashboard → Project Settings → Environment Variables. Select Production environment.
# Core
NEXTAUTH_URL=https://yoursaas.com
NEXTAUTH_SECRET=<generate with: openssl rand -base64 32>
# Database
DATABASE_URL=postgresql://...
# Stripe (LIVE keys, not test)
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_WEBHOOK_SECRET= # Set after step 4
# Email
RESEND_API_KEY=re_...
EMAIL_FROM=YourSaaS <noreply@yoursaas.com>
# OAuth (if using)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Error tracking
SENTRY_DSN=
NEXT_PUBLIC_SENTRY_DSN=
Critical: Never use test Stripe keys in production. sk_test_ keys will silently fail on real payments.
Step 4: Stripe Webhook Registration
# Register your production webhook endpoint in Stripe dashboard:
# Stripe Dashboard → Developers → Webhooks → Add endpoint
# URL: https://yoursaas.com/api/stripe/webhook
# Or via CLI:
stripe listen --forward-to https://yoursaas.com/api/stripe/webhook
Events to subscribe to (minimum):
checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
invoice.payment_succeeded
invoice.payment_failed
Verify your webhook handler checks the signature:
// app/api/stripe/webhook/route.ts
const sig = req.headers.get('stripe-signature')!;
const body = await req.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return new Response('Webhook signature verification failed', { status: 400 });
}
Step 5: Custom Domain
# In Vercel dashboard → Project → Domains → Add
# Add: yoursaas.com and www.yoursaas.com
# DNS records to add at your registrar:
# A record: @ → 76.76.21.21 (Vercel IP)
# CNAME record: www → cname.vercel-dns.com
Update environment variables after domain is live:
NEXTAUTH_URL=https://yoursaas.com # Was localhost:3000
Step 6: Email Domain Verification (Resend)
# In Resend dashboard → Domains → Add Domain
# Add DNS records:
# TXT record: resend._domainkey.yoursaas.com → (DKIM key from Resend)
# TXT record: yoursaas.com → v=spf1 include:amazonses.com ~all
# Verify in Resend dashboard (takes 10-30 min to propagate)
Pre-Launch Verification Checklist
# Auth
[ ] Sign up with email works → welcome email received
[ ] Google OAuth login works
[ ] Sign out works
[ ] Password reset email received
# Payments
[ ] Checkout page loads with correct prices
[ ] Test purchase with: 4242 4242 4242 4242 (Stripe test card)
[ ] Switch to LIVE mode and verify a real $1 test charge
[ ] Webhook fires and subscription created in DB
[ ] Billing portal loads via Manage Billing button
# Core Features
[ ] Dashboard loads after login
[ ] Feature gating works (Pro features blocked for free users)
[ ] Settings page saves
# Error Handling
[ ] 404 page renders (visit /does-not-exist)
[ ] Error boundary works (no blank screens)
[ ] Sentry receives test event: Sentry.captureException(new Error("test"))
# Performance
[ ] Lighthouse score ≥ 90 on /
[ ] Core Web Vitals: LCP < 2.5s
[ ] No console errors on load
Common Deployment Failures
| Error | Cause | Fix |
|---|---|---|
NEXTAUTH_URL mismatch | Env var not updated for production domain | Set to https://yoursaas.com |
| Prisma migration errors | Using migrate dev instead of migrate deploy | Use prisma migrate deploy in build |
| OAuth redirect mismatch | Google/GitHub OAuth not updated | Add production URL to OAuth app |
| Stripe webhooks 401 | Wrong webhook secret | Regenerate in Stripe and update env var |
| Email from Resend sandbox | Domain not verified | Complete Resend DNS verification |
Zero-Downtime Database Migrations
The most dangerous part of production deployment is applying database migrations without breaking the running application. The naive approach — run prisma migrate deploy then deploy new code — creates a brief window where either old code is reading a changed schema or new code is reading an old schema.
The safe approach for zero-downtime migrations is the expand-contract pattern:
Expand phase (deploy 1): Add new columns/tables without removing old ones. Both old and new code can run simultaneously.
Migration phase: Write code that works with both old and new schema. Deploy this version.
Contract phase (deploy 2): Remove old columns after all instances run the new code.
For Neon specifically, the branching feature makes this manageable:
# 1. Create a branch from production
neonctl branches create --name migration-test --parent main
# 2. Apply your migration to the branch
DATABASE_URL=<branch-connection-string> npx prisma migrate deploy
# 3. Point a test deployment to the branch and verify
# 4. Run your migration against production
DATABASE_URL=<prod-connection-string> npx prisma migrate deploy
# 5. Deploy new code
vercel deploy --prod
For most indie SaaS, this level of rigor is optional — a 10-second downtime during a migration is acceptable when you have hundreds of users. It becomes necessary when you have thousands of concurrent users or SLAs that require 99.9% uptime.
Monitoring: What to Set Up Before Launch
Launching without monitoring is flying blind. The minimum setup:
Error tracking (Sentry free tier): Add Sentry in 10 minutes and catch every runtime error with full stack traces. Without it, you'll learn about errors when users email you.
npx @sentry/wizard@latest -i nextjs
# Guides you through Sentry configuration automatically
Uptime monitoring (Better Uptime or Checkly free tier): Get notified by SMS/email when your site is down. The Vercel dashboard shows availability but doesn't alert you.
Log aggregation (Vercel Log Drains or Axiom): Production logs are essential for debugging. Set up log draining before you need it — you can't retroactively capture logs from incidents that already happened.
Alerting thresholds: Set up alerts for:
- Error rate > 1% in any 5-minute window
- P95 response time > 2 seconds
- Database connection pool exhausted
- Failed webhook delivery from Stripe
These four monitors prevent "I didn't know anything was wrong until 3 days later" situations that are common in early-stage SaaS.
Securing Your Production Environment
Security steps that are often skipped in the rush to launch:
1. Lock down Vercel preview deployments: By default, Vercel preview deployments (for PRs) are publicly accessible. If your preview deployments share environment variables with production, they expose your production API keys to anyone with the URL.
Fix: In Vercel → Settings → Deployment Protection, enable Vercel Authentication for preview deployments.
2. Rotate secrets if exposed: If you accidentally committed a .env file or exposed a secret in a PR, rotate all secrets immediately. Change the Stripe API key, NEXTAUTH_SECRET, database password, and any OAuth client secrets. Assume exposed credentials are compromised.
3. Enable Vercel Bot Protection: Vercel Pro includes bot protection that blocks malicious scrapers and DDoS attempts. Free tier doesn't include it — at minimum, add Upstash rate limiting to your auth endpoints.
4. Set Content Security Policy headers: Prevent XSS attacks with a CSP header:
// next.config.ts
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://js.stripe.com;
frame-src https://js.stripe.com;
connect-src 'self' https://api.stripe.com;
`;
const securityHeaders = [
{ key: 'Content-Security-Policy', value: ContentSecurityPolicy.replace(/\n/g, '') },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
];
Post-Launch: The First 48 Hours
The first 48 hours after launch are when most deployment problems surface:
Monitor your error tracker hourly: New users hit code paths that weren't tested. Sentry will surface them.
Check Stripe webhook delivery: Go to Stripe Dashboard → Webhooks → Your endpoint → Recent deliveries. Any failed deliveries (4xx or 5xx responses) indicate webhook processing bugs.
Watch your database connection pool: Serverless functions can exhaust database connections at launch traffic spikes. If you see connection pool errors, add ?connection_limit=1 to your DATABASE_URL for serverless environments.
Have a rollback plan: Vercel makes rollbacks trivial — go to Deployments, find the previous deployment, and promote it. Database migrations are harder to roll back. Know which migrations are reversible before you run them.
Watch your email deliverability: Send a test email from your production environment on launch day and verify it lands in the inbox (not spam). Check Resend's dashboard for bounce rates.
Announce in the right channels: Post in relevant communities (Indie Hackers, relevant Slack groups, your email list) on launch day. Having real users interact with your app in the first 48 hours surfaces issues that staging never catches. User-reported bugs in the first 48 hours are expected — respond quickly and fix visibly to build trust with early adopters.
CI/CD: Automating Deployments
Manual vercel --prod deploys are fine for solo developers at launch, but become a bottleneck as the team grows and release frequency increases. A minimal GitHub Actions pipeline catches build failures before they reach production.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npx prisma generate
- run: npm run typecheck
- run: npm run lint
- run: npm run build
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
NEXTAUTH_URL: https://yoursaas.com
Vercel auto-deploys from GitHub pushes when connected — no separate deploy step needed in CI. The value of the CI pipeline is catching TypeScript errors, lint failures, and build failures on pull requests before merge, not triggering the deployment itself.
For database migrations, run prisma migrate deploy in a separate CI step that only runs on merges to main, after the build passes. Never run migrations on pull request branches — migrations are irreversible and running them against your production database from a feature branch is a serious operational risk.
Connection Pooling in Production
Serverless functions create a new database connection on every invocation. At modest traffic, this is manageable. At scale, it exhausts your database's connection limit. A Neon free tier database supports 100 connections — 100 simultaneous Vercel function invocations can hit that limit easily.
The fix is a connection pooler that sits between your serverless functions and the database, reusing connections across invocations.
Neon: Built-in connection pooling via ?pgbouncer=true in the connection string. Use the pooled connection string from the Neon dashboard for your application code. Use the direct (non-pooled) connection string for Prisma migrations.
# .env.production
# Use pooled connection for app queries
DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb?pgbouncer=true&connection_limit=1"
# Direct connection for migrations only
DIRECT_URL="postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb"
// schema.prisma — separate URLs for pooled vs direct
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
The connection_limit=1 parameter is critical for serverless — it tells Prisma to use at most one connection per function instance, preventing connection storms at high concurrency.
Environment Parity: Staging and Production
Bugs that only appear in production are the most expensive to fix. They require debugging under real traffic with limited access to reproduction steps. The standard mitigation is a staging environment that matches production as closely as possible.
Vercel makes this straightforward: create a second Vercel project pointing at the same GitHub repository, using a staging branch instead of main. Set environment variables identical to production, but point to a separate Neon branch for the database. Stripe test mode keys in staging; live keys in production.
The deployment workflow then becomes: push to staging branch → auto-deploys to staging → verify → merge staging to main → auto-deploys to production. Database migrations run against the staging database first, catching schema errors before they affect production data.
This parity setup takes one afternoon to configure and prevents a class of production-only bugs that cost far more in debugging and user trust. For solo developers, even a basic staging environment is worth the setup investment. Neon's database branching feature makes staging database setup especially low-friction — branching from production creates an isolated copy in seconds, allowing migration testing without manual database provisioning.
Compare boilerplates by deployment complexity in our best open-source SaaS boilerplates guide.
See our guide to SaaS observability for monitoring and alerting setup.
Browse full-stack TypeScript boilerplates to choose the right foundation before deployment.
Check out this boilerplate
View ShipFaston StarterPick →