CI/CD Setup for SaaS Boilerplates 2026
TL;DR
GitHub Actions + Vercel is the default CI/CD stack for Next.js SaaS in 2026. Vercel handles preview and production deployments automatically on push. GitHub Actions handles tests, type checking, and linting before code reaches production. Total setup time: 0.5–1 day. This guide covers the workflow that ships at most SaaS startups.
The Baseline: What Vercel Gives You Free
Before adding GitHub Actions, understand what Vercel already does:
- Preview deployments on every PR (unique URL per branch)
- Production deployments on push to
main - Build caching (incremental builds for Next.js)
- Environment variable management per environment (preview/production)
For most boilerplates, this is enough to start. Add GitHub Actions when you need tests to block merges. If you're working through the broader setup process, the boilerplate to launch in 7 days guide covers the full Day 1 environment setup and how CI fits into that timeline.
GitHub Actions: Test-on-PR Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run type-check
- name: Lint
run: npm run lint
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ env.DATABASE_URL }}
- name: Run tests
run: npm run test
env:
DATABASE_URL: ${{ env.DATABASE_URL }}
NEXTAUTH_SECRET: test-secret
NEXTAUTH_URL: http://localhost:3000
Package.json Scripts to Add
{
"scripts": {
"type-check": "tsc --noEmit",
"lint": "next lint",
"test": "vitest run",
"test:e2e": "playwright test",
"test:watch": "vitest"
}
}
Environment Variables in GitHub Actions
Secrets and variables are set in GitHub repository settings → Secrets and variables → Actions.
# Access secrets in your workflow
- name: Run tests
run: npm run test
env:
DATABASE_URL: ${{ secrets.DATABASE_URL_TEST }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY_TEST }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
Best practice: Use separate Stripe test keys and a dedicated test database. Never use production credentials in CI.
Preventing Deploy on Test Failure
Vercel deploys even if tests fail unless you explicitly block it. Two approaches:
Option 1: Vercel Ignored Build Step
# In Vercel project settings → Git → Ignored Build Step
# This command runs before the build; non-zero exit cancels deploy
npx tsc --noEmit && npx eslint . --max-warnings 0
Option 2: GitHub Actions + Vercel Integration
Disable Vercel's automatic GitHub integration and trigger deploys from Actions instead:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
needs: [test] # Only runs if 'test' job succeeds
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Database Migrations in CI
Running migrations safely in CI:
- name: Run migrations
run: npx prisma migrate deploy
# Use 'migrate deploy' (not 'migrate dev') in CI
# 'deploy' applies existing migrations
# 'dev' generates new migrations (interactive, not for CI)
For production deployments, run migrations before the app starts:
// package.json — run migrations on Vercel build
{
"scripts": {
"build": "prisma generate && prisma migrate deploy && next build"
}
}
Preview Environment Variables
Vercel lets you set different env vars per environment. For preview deployments, use test API keys:
# Set in Vercel dashboard → Environment Variables
# Check "Preview" environment only
STRIPE_SECRET_KEY=sk_test_... # Test key for preview
RESEND_API_KEY=re_test_... # Test key for preview
DATABASE_URL=postgres://... # Separate preview database (e.g., Neon branch)
Neon database branching (works great with preview deploys):
# Create a database branch per PR
- name: Create Neon branch
uses: neondatabase/create-branch-action@v5
id: create-branch
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: preview/pr-${{ github.event.number }}
api_key: ${{ secrets.NEON_API_KEY }}
- name: Set branch URL
run: echo "DATABASE_URL=${{ steps.create-branch.outputs.db_url }}" >> $GITHUB_ENV
E2E Tests with Playwright
For critical flows (auth, checkout):
# .github/workflows/e2e.yml
name: E2E Tests
on:
pull_request:
branches: [main]
jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Build app
run: npm run build
env:
DATABASE_URL: ${{ secrets.DATABASE_URL_TEST }}
- name: Run Playwright tests
run: npx playwright test
env:
BASE_URL: http://localhost:3000
DATABASE_URL: ${{ secrets.DATABASE_URL_TEST }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
CI Pipeline Costs
| Pipeline | Free Tier | Paid |
|---|---|---|
| GitHub Actions | 2,000 min/month | $0.008/min |
| Vercel deployments | Unlimited (Hobby) | $20/mo (Pro) |
| Neon branches | 10 branches | $19/mo |
For most early-stage SaaS, the free tiers are sufficient. A typical CI run (type-check + lint + unit tests) takes 2-4 minutes.
Minimal Starting Point
If you want CI without the complexity, start here:
# .github/workflows/ci.yml — bare minimum
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run type-check
- run: npm run lint
This runs in ~90 seconds and catches the most common issues. Add tests and database when you have them.
Time Budget
| Task | Duration |
|---|---|
| Vercel project setup + env vars | 1 hour |
| Basic CI workflow (type-check + lint) | 0.5 day |
| Test database setup | 0.5 day |
| Unit test pipeline | 0.5 day |
| E2E tests (optional) | 1 day |
| Total (minimal) | 1 day |
Why CI Catches What Manual Review Misses
TypeScript catches type errors but doesn't catch everything. Two categories of bugs consistently escape developer review but get caught reliably in CI.
Missing environment variable usage. A developer adds a new API integration, hardcodes the API key in development, and forgets to add it to the .env.example and the deployment environment variables. The code passes type checking and linting, deploys to production, and fails at runtime for every user who calls that feature. CI that runs the actual application can catch this before it reaches production if the test environment includes env validation.
Prisma migration schema drift. Developers sometimes modify the schema.prisma file and run prisma db push locally (which applies changes without creating a migration file) rather than prisma migrate dev (which creates a versioned migration). The local database works. The CI environment, running from migration files, doesn't have the schema change. The CI job fails on prisma migrate deploy with a drift error — catching a production deployment that would have silently broken database operations.
Both of these are subtle bugs that code review doesn't reliably catch because they require knowing what's missing, not reviewing what's present. CI catches them structurally.
Common CI Configuration Mistakes
Three configuration mistakes appear repeatedly in boilerplate-based projects setting up CI for the first time.
Using npm install instead of npm ci. npm install may upgrade packages within semver ranges if the lockfile is stale. npm ci installs exactly what's in package-lock.json — this is the correct choice for CI. The difference matters when a dependency has a minor version update that introduces a behavior change.
Caching node_modules without cache key invalidation. Caching dependencies speeds up CI runs significantly but requires a cache key that includes the lockfile hash. A cache key of ${{ runner.os }}-node without the lockfile hash will serve stale cached dependencies after package-lock.json changes, causing confusing test failures in CI while passing locally.
# Correct cache configuration:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # This correctly uses package-lock.json as cache key
Running migrations in CI with prisma migrate dev. The migrate dev command is for development — it prompts interactively, generates new migrations, and modifies schema files. It's not suitable for CI. Use prisma migrate deploy, which applies existing migration files without generating new ones. This distinction is in the workflow above but frequently gets changed by developers who see it failing and switch to the command they use locally.
Monitoring After Deployment
CI handles pre-deployment validation. Post-deployment monitoring catches problems that only emerge in production conditions.
Sentry is the standard error tracking tool for Next.js SaaS. The free tier allows 5,000 errors per month with 30-day retention — sufficient for early-stage products. Sentry's Next.js SDK captures both client-side and server-side errors, including unhandled promise rejections in Route Handlers and Server Components. The setup takes about 30 minutes and should be added on Day 1 of a new project.
Vercel Analytics provides page view and web vitals data without requiring additional setup. The data is available immediately in the Vercel dashboard. For user behavior beyond page views, PostHog's free tier (1 million events/month) integrates cleanly with Next.js and provides event tracking, funnel analysis, and session recordings.
The combination of Sentry + Vercel Analytics + PostHog covers the monitoring surface for an early-stage SaaS without requiring paid monitoring infrastructure. When production errors spike — visible in Sentry — the root cause investigation starts with the most recent deployment. GitHub Actions' deployment records and Vercel's deployment history make it straightforward to identify which commit introduced an issue.
When to Add More CI/CD Complexity
The configuration above handles the needs of most SaaS products through significant scale. Additional complexity is justified when specific conditions appear.
Add staging environments when you have a non-technical team member who needs to approve changes before production, or when you need to test integration with external APIs using production-tier credentials (not test mode). Most early-stage SaaS products don't need staging — the cost is additional environment management work for each deployment.
Add deployment notifications (Slack or email) when the team has more than one developer who needs to know when deployments happen. The GitHub Actions Slack action takes about 30 minutes to configure and eliminates the "when did that deploy?" question.
Add branch protection rules (Settings → Branches → Require status checks to pass) when the main branch is shared among multiple developers. This prevents direct pushes to main and requires CI to pass before merging. For solo founders, this is optional overhead. For teams of 3+, it's table stakes.
Deploying a Boilerplate: What Happens on the First Push
The first time you push a boilerplate to Vercel and trigger a production build, a predictable sequence of issues surfaces. Understanding them in advance saves the debugging time.
Build-time environment variables. Vercel separates environment variables into runtime (available in API routes and server components) and build-time (available during next build). Variables needed during the build — like NEXT_PUBLIC_ prefixed variables and any that are accessed in static generation — must be set in Vercel before the first build runs. A common first-deploy failure is a missing NEXT_PUBLIC_URL that the boilerplate references for generating absolute URLs during static generation.
Database connectivity from Vercel Edge. Some boilerplates use Prisma with the standard TCP-based PostgreSQL connection. Vercel's serverless functions support this on Node.js runtime, but Vercel's Edge runtime (used by middleware and some Route Handlers) requires a connection pooler like PgBouncer or Neon's serverless driver. Most modern boilerplates use Neon's @neondatabase/serverless driver or Prisma's Accelerate service to handle this. If your boilerplate uses the standard Prisma PostgreSQL driver without a pooler and you're seeing connect ETIMEDOUT errors in production, this is the cause.
Prisma generate in production builds. The prisma generate command creates the Prisma client TypeScript types from your schema. This command must run before next build in your deployment build script. Many boilerplates include this in their package.json build script, but if yours doesn't, add prisma generate && next build as your build command in Vercel project settings.
These are first-deploy issues that CI catches early when preview environments are properly configured with the same environment variables as production. Once the first successful production deploy goes through, CI primarily catches regression bugs rather than configuration issues.
For boilerplate recommendations with CI/CD considerations in mind, see StarterPick's comparison page. The how to deploy a SaaS boilerplate to production guide covers the first deployment in more detail. Teams also benefit from the zero-downtime database migrations guide once CI pipelines are in place and the product has real users.
Find boilerplates with CI/CD configured out of the box on StarterPick.
Review ShipFast and compare alternatives on StarterPick.