How to Set Up CI/CD for Your SaaS Boilerplate (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.
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 |
Find boilerplates with CI/CD configured out of the box on StarterPick.
Check out this boilerplate
View ShipFast on StarterPick →