Skip to main content

How to Set Up CI/CD for Your SaaS Boilerplate (2026)

·StarterPick Team
cicdgithub-actionsverceldevopsguide2026

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

PipelineFree TierPaid
GitHub Actions2,000 min/month$0.008/min
Vercel deploymentsUnlimited (Hobby)$20/mo (Pro)
Neon branches10 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

TaskDuration
Vercel project setup + env vars1 hour
Basic CI workflow (type-check + lint)0.5 day
Test database setup0.5 day
Unit test pipeline0.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 →

Comments