Skip to main content

CI/CD Setup for SaaS Boilerplates 2026

·StarterPick Team
Share:

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

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

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.

The SaaS Boilerplate Matrix (Free PDF)

20+ SaaS starters compared: pricing, tech stack, auth, payments, and what you actually ship with. Updated monthly. Used by 150+ founders.

Join 150+ SaaS founders. Unsubscribe in one click.