For a while my "deployment process" was pushing to main and hoping Vercel's build did not fail. It usually worked, until a project grew past a single developer and untested code started reaching production because nothing ran before the deploy actually happened.
Here is the setup I use now, simple enough to not slow a solo project down, solid enough to actually catch problems before they ship.
1. Vercel Handles the Deploy, GitHub Actions Handles the Checks
This is the part people overcomplicate. Vercel already builds and deploys automatically on every push, no extra configuration needed for that part. What is usually missing is anything running before that deploy to catch a broken build, a failing test, or a type error before it reaches production.
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Type check
run: npx tsc --noEmit
- name: Lint
run: npm run lint
- name: Test
run: npm run test -- --run
- name: Build
run: npm run build
This runs on every pull request and every push to main. A broken build or failing test shows up as a red X directly on the PR, before Vercel ever tries to deploy it, and before a teammate merges something that looks fine locally but breaks in CI.
2. Why the Build Step Matters Even Though Vercel Also Builds
Running npm run build in GitHub Actions looks redundant since Vercel builds the project too. The difference is timing. Vercel's build happens after the merge, when the broken code is already on main. Catching the same failure in CI on the pull request means it never reaches main at all.
3. Branch Protection to Actually Enforce This
A CI check that runs but does not block anything is just a suggestion. In your GitHub repo settings, under branch protection rules for main:
- Require status checks to pass before merging
- Select the
checksjob from the workflow above as required - Optionally require at least one approving review
Without this, CI can fail loudly on a pull request and someone can still click merge anyway. Branch protection is what turns the checks from advisory into an actual gate.
4. Preview Deployments for Every Pull Request
Vercel does this automatically once your GitHub repo is connected, every pull request gets its own live preview URL, posted directly as a comment on the PR. This alone catches a large category of bugs that only show up when something is actually deployed, not just running locally.
For a client project, sending the preview URL directly instead of a screenshot or a description means the client can click around a real, working version of the change before it goes live.
5. Environment Variables Across Environments
A common mistake is using the same environment variables for local development, preview deployments, and production, especially with things like a Stripe key or a database URL that should genuinely differ between them.
In Vercel's project settings, environment variables can be scoped separately:
MONGODB_URI → Production only (real database)
MONGODB_URI → Preview (staging database, separate from production data)
MONGODB_URI → Development (local .env.local, not in Vercel at all)
STRIPE_SECRET_KEY → Production (live key)
STRIPE_SECRET_KEY → Preview (test key)
This matters most for Stripe and the database. Without separate values, a preview deployment testing a checkout flow could accidentally hit your real Stripe account or write test data into your production database.
6. Running Database Migrations Safely
For projects with schema changes that need to run before new code goes live, a manual migration script triggered separately from the deploy is safer than running it automatically on every build:
# .github/workflows/migrate.yml
name: Run Migration
on:
workflow_dispatch: # manually triggered, not automatic
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run migrate
env:
MONGODB_URI: ${{ secrets.PRODUCTION_MONGODB_URI }}
workflow_dispatch means this only runs when manually triggered from the GitHub Actions tab, not automatically on every push. Schema changes against a production database are exactly the kind of thing that benefits from a deliberate, manual trigger rather than happening silently as part of every deploy.
7. Rolling Back a Bad Deploy
Even with checks in place, something can still slip through, a runtime bug that only shows up under real production data or traffic patterns CI never sees. Vercel keeps every previous deployment available, and rolling back is a matter of promoting the last known-good deployment back to production from the dashboard, no new build required, no waiting on a fix to be written first.
Worth doing this once on a low-stakes project just to know the process before you actually need it under pressure.
8. Keeping the CI Workflow Fast
A CI check that takes ten minutes gets ignored or worked around eventually. A few things that keep it quick:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # caches node_modules based on package-lock.json
Caching node_modules based on the lockfile hash means npm ci reuses the cache instead of reinstalling every dependency from scratch on every single run, which is usually the slowest part of the whole workflow.
Summary
| Piece | Handles |
|---|---|
| GitHub Actions CI workflow | Type checking, linting, tests, and build, before merge |
| Branch protection | Actually enforcing that CI passing is required, not optional |
| Vercel preview deployments | A real, clickable URL for every pull request |
| Scoped environment variables | Separate keys for production, preview, and local, especially Stripe and the database |
| Manual migration workflow | Deliberate, controlled schema changes instead of automatic ones on every deploy |
| Rollback via Vercel dashboard | A fast recovery path when something ships broken anyway |
None of this is complicated infrastructure. It is closer to a checklist wired into GitHub, catching the same mistakes that used to only show up after a client noticed something broken in production.
I run this exact setup, CI checks, branch protection, scoped environment variables, on every client project with more than one person touching the code.
Get the templates: https://pixelanas.gumroad.com
What does your deploy process look like, direct to Vercel, or something with checks in front of it? Drop it below 👇
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (0)