Every Next.js SaaS should have a CI pipeline that runs type checks, linting, and tests before code reaches production. Without it, a broken build or failed type check makes it to Vercel — where you find out from a broken production site instead of a failed check.
Here's how to set up a practical GitHub Actions CI pipeline for a Next.js SaaS, with the jobs that actually matter.
What your pipeline should check
For a Next.js SaaS, run these in CI:
- TypeScript:
npx tsc --noEmit - ESLint:
npm run lint - Unit tests:
npm test (if you have them) - Build check:
npm run build (optional but catches import errors)
Run type checking and linting first — they're fast (under 30 seconds). Only run the full build in CI if you have a reason (Vercel already runs it on deploy, so it's redundant unless you need artifacts).
Basic CI workflow
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
typecheck:
name: TypeScript
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npx tsc --noEmit
lint:
name: ESLint
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 lint
Each job runs independently and in parallel — TypeScript and ESLint check simultaneously. Both must pass before the PR can be merged.
Adding environment variables for CI
If your build or tests need environment variables (API keys for integration tests, or env vars required for the build), add them as GitHub repository secrets:
- GitHub repo → Settings → Secrets and variables → Actions
- Add each secret (STRIPE_SECRET_KEY, SUPABASE_SERVICE_ROLE_KEY, etc.)
Reference them in the workflow:
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
# Add all vars your build needs...
For type checking only (tsc --noEmit), you usually don't need real API keys — the types don't depend on runtime values.
Adding unit tests with Vitest
If you have unit tests, add a test job:
test:
name: Unit tests
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 test
In package.json, configure the test script:
{
"scripts": {
"test": "vitest run"
}
}
Use vitest run (not just vitest) in CI to run tests once and exit, rather than in watch mode.
Caching node_modules
The cache: "npm" option in setup-node caches the npm cache directory, which speeds up npm ci on repeat runs. Without caching, npm downloads all packages every time — which adds 30–60 seconds to every CI run.
GitHub Actions automatically invalidates the cache when package-lock.jsonchanges.
Branch protection rules
CI checks are only useful if you enforce them. Add branch protection to main:
- GitHub repo → Settings → Branches → Add rule
- Branch name pattern:
main - Enable: “Require status checks to pass before merging”
- Add your CI jobs as required checks:
TypeScript, ESLint - Enable: “Require branches to be up to date before merging”
With this configuration, PRs can't be merged until all CI jobs pass. Direct pushes to main also go through the checks.
Vercel integration
Vercel automatically creates preview deployments for each PR and deploys main to production on merge. This gives you:
- A preview URL for every PR (share with reviewers or test manually)
- Automatic production deployment when CI passes and the PR merges
- Build logs in the Vercel dashboard if deployment fails
You don't need a separate deployment job in GitHub Actions — Vercel's GitHub integration handles it. Just make sure your CI checks run before reviewers can approve merges.
Full workflow example
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
typecheck:
name: TypeScript
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npx tsc --noEmit
lint:
name: ESLint
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 lint
test:
name: Unit tests
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 test
What GetLaunchpad adds
GetLaunchpad doesn't include a GitHub Actions workflow by default (Vercel handles deployment automatically), but the codebase is set up to pass CI from day one:tsconfig.json is strict, .eslintrc is configured, and all utilities are type-safe. Adding the workflow above requires only creating the .github/workflows/ci.yml file.