Fast feedback loops
The CI/CD setup that keeps the codebase honest: local checks, GitHub Actions, and Vercel working together so nothing broken reaches main.
What CI/CD actually does for a solo developer
In a solo development operation, CI/CD isn't about gatekeeping — it's about catching the mistakes you didn't notice while moving fast. The goal is a feedback loop tight enough that nothing broken ever reaches main, and nothing that reaches production surprises you.
The failure mode CI is designed to prevent: you edit something quickly, forget to run lint, push, and then discover the break hours later when you've context-switched onto something else. Or worse, discover it in production. A good pipeline eliminates that gap with zero extra discipline required — the machine checks so you don't have to remember.
Local checks: the first line of defence
The fastest feedback is local. Before anything else, these two commands cover the full quality surface of this stack:
# Check everything locally before pushing npm run lint # ESLint + Prettier (one command — both run together) npm run build # Full production build, catches type errors too
npm run lint runs ESLint and Prettier together — one command, both checks, auto-fixable with npm run lint:fix. We don't run a separate tsc --noEmit step because next build runs the TypeScript compiler as part of the build process. One command covers types, tree shaking, bundle analysis, and output.
Running both locally before every push is the habit that eliminates most CI failures. CI then becomes a safety net for the moments you forget, not the primary gate.
GitHub Actions
Even as a solo developer, a CI pipeline is worth the 20 minutes it takes to set up. It catches things local runs miss: forgetting to lint after a quick edit, type errors that only surface in a clean install environment, and build failures caused by environment variables you've been papering over locally.
Here's the workflow we use, living at .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
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
- name: Lint
run: npm run lint
- name: Build
run: npm run build
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY }}A few notes on this setup:
npm ci(notnpm install) installs exactly what's in the lockfile, so the CI environment is deterministic. A dependency that works locally but isn't in the lockfile will fail here.- Environment variables that are required for
next buildneed to be in GitHub Secrets. Expose them viaenv:on the build step, not globally, so they're scoped to where they're needed. - Lint runs before build. Fail fast — there's no point spending 60 seconds on a build when there are lint errors.
Choosing a deployment platform
Both Vercel and DigitalOcean App Platform have excellent built-in CD. Connect a GitHub repository and you get automatic preview deployments on every pull request and a production deploy on every push to main — no extra pipeline configuration required. That covers the full loop:
- GitHub Actions catches bad code before merge
- Preview deployments let you see changes in a real environment before they go live
- Production deploys on merge — no manual step, no forgetting to deploy
- Instant rollback from the dashboard if something slips through
For most Next.js projects — especially solo or small-team work — Vercel is the right default. Zero infrastructure knowledge required, first-class Next.js support, and the free tier covers a lot of ground.
DigitalOcean App Platform is worth considering when your project is more complex: multiple services, background workers, custom runtimes, or tighter cost control at scale. It gives you more configuration surface and more control over the underlying infrastructure, but that comes with a real learning curve. It's not the path of least resistance — it's the path you take when you know what you need and why.
For enterprise clients already deep in the AWS ecosystem, AWS Amplify is the natural fit. It plugs into the same IAM roles, VPCs, and compliance boundaries the rest of their infrastructure lives in — which matters when procurement, security, and auditing are already AWS-shaped. The trade-off is the same as everything else in the AWS ecosystem: more power, more surface area, and a steeper ramp to get productive. If the client is already there, it's worth using. If they're not, it's rarely worth adopting just for deployment.
Database migrations: the exception
Schema migrations with Drizzle + Supabase don't run in CI. This is intentional. Database changes are high-stakes — they're hard to reverse, and a bad migration in an automated pipeline is worse than a missed deploy. The workflow is always manual:
# Local — always in this order npm run db:generate # Preview the migration SQL, review it npm run db:migrate # Apply to production (DATABASE_URL_UNPOOLED) # Never run in CI, never run db:push against production # db:push skips migration history — treat it as dev-only
Generate the migration locally, review the SQL, apply it manually with the production connection string. Never automate the apply step. The 30 seconds it takes to review a migration has prevented more than one data loss incident.
The throughline
The value of this pipeline isn't in any individual piece — it's in having no gap between writing code and knowing whether it works. Local checks for instant feedback, GitHub Actions for the safety net, Vercel for zero-friction deployment. The feedback loop is tight enough that moving fast doesn't mean accumulating risk.
That's the CI/CD side of vibe engineering: the discipline is built into the process, not the person.
Have a project in mind?
Book a free 30-minute call. No commitment, just a conversation.
Book a free 30-min callPrefer to write? Send us a message — we respond within 24 hours.