GitHub Actions + Vercel deploy pipeline for UK indie hackers in 2026

Key Takeaways
- Do NOT replace Vercel's native deploy with 'vercel deploy --prod' in Actions for a solo project -- use Vercel git integration for deploys, Actions for checks.
- Production secrets (Stripe, Resend, Supabase) live in Vercel only. Never pull them into GitHub Actions.
- Set branch protection on main: require CI to pass, require linear history, never allow force push.
- Upgrade to Vercel Pro the moment you have a paying user -- Hobby bans commercial use.
- Run Playwright E2E against the Vercel preview URL, pinned to the lhr1 (London) region for accurate UK performance data.
TL;DR
For a UK indie hacker shipping a Next.js SaaS in 2026, the correct pipeline is: GitHub Actions for tests and checks, Vercel git integration for the actual deploy. Do not replace Vercel's native deploy with vercel deploy --prod running inside an Actions step. That is the single most common mistake, it adds latency, it costs you Actions minutes, and it breaks Vercel's preview URL system. Get this one opinion locked in before you read anything else.
Everything below is the practical detail behind that opinion.
When to use Actions vs Vercel native
The division of responsibility is simple once you say it out loud.
| Concern | Tool |
|---|---|
| Deploy to preview (on PR) | Vercel git integration |
| Deploy to production (on main merge) | Vercel git integration |
| Typecheck | GitHub Actions |
| Lint | GitHub Actions |
| Unit tests | GitHub Actions |
| E2E tests (against preview URL) | GitHub Actions + Playwright |
| Lighthouse CI | GitHub Actions |
| Bundle size delta PR comment | GitHub Actions |
| Schema generation | GitHub Actions |
| Security scanning / SBOM | GitHub Actions |
| Rollback | Vercel dashboard (Instant Rollback) |
Vercel native handles preview and production deploys automatically the moment you connect your repo. It is faster than Actions-triggered deploys because it runs on Vercel's own infrastructure without queuing behind your Actions runner. It also produces the preview URL that your E2E tests need.
GitHub Actions handles everything that should gate the deploy. A status check that fails blocks the merge to main. Vercel never gets a new commit to deploy. That is the architecture.
The only time you would run vercel deploy inside Actions is if you are doing something non-standard, like deploying a branch that does not exist in git (unusual) or deploying to a second Vercel org (multi-tenant setups). For a solo UK SaaS, that never comes up.
The minimal CI workflow
Below is a production-ready .github/workflows/ci.yml for a UK indie hacker on Node 22 LTS and pnpm. Copy it, swap in your own pnpm test command, and you are done.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
test:
name: Unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test --coverage
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
build:
name: Build smoke check
runs-on: ubuntu-latest
env:
# Use stub values for build-time env vars -- no production secrets
NEXT_PUBLIC_APP_URL: https://preview.example.co.uk
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
Three things worth noting:
concurrencywithcancel-in-progress: truekills the previous run on the same branch when you push again. Saves Actions minutes on rapid iteration.- Jobs run in parallel by default. Typecheck, lint, and unit tests all start at the same time. Total wall-clock time on a typical Next.js SaaS is 3-4 minutes.
- The build smoke check uses stub env vars. No production secrets in Actions -- more on that next.
UK-specific secret hygiene
This is the section most tutorials skip, and it is the one that causes security incidents.
Rule: production secrets never touch GitHub Actions.
| Secret | Where it lives | Why |
|---|---|---|
STRIPE_SECRET_KEY (restricted key) | Vercel environment variables only | Only needed at runtime, not build time |
RESEND_API_KEY | Vercel environment variables only | Same reason |
SUPABASE_SERVICE_ROLE_KEY | Vercel environment variables only | Elevated privilege -- keep it off Actions runners |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Vercel + .env.example (safe to expose) | Public key, fine to document |
CODECOV_TOKEN | GitHub repo secret | CI tooling only |
LIGHTHOUSE_CI_TOKEN | GitHub repo secret | CI tooling only |
CLOUDFLARE_API_TOKEN | GitHub repo secret (DNS scope only) | Only if you automate DNS with wrangler |
The distinction that matters: GitHub repo secrets are available to any workflow, including those triggered by pull requests from forks. If a stranger forks your public repo and opens a PR, Actions runs your workflow against their code -- with access to your secrets. Vercel env vars never touch that flow.
For the build smoke check in CI, stub out any env var that the build requires but that has no production value at build time. A NEXT_PUBLIC_APP_URL pointing to a placeholder is fine. Your Stripe key is not.
If you use GitHub Environments (distinct from repo secrets), you can restrict secrets to specific branches -- e.g., only main can access a production environment. That is worth setting up even for a solo project. It adds an extra guardrail against accidental exposure on feature branches.
Branch protection for solo devs
Solo devs skip branch protection because there is nobody to stop. That is the mistake. You are not protecting yourself from other people; you are protecting yourself from yourself at 2am, when you git push -f main after a botched rebase because you were tired and in a hurry.
Recommended settings for the main branch on GitHub:
Require status checks to pass before merging: YES
-- Required checks: typecheck, lint, test, build
Require branches to be up to date before merging: YES
Require linear history: YES
Allow force pushes: NO (for main)
Allow force pushes: YES (for feature/* branches -- you need this for rebase workflows)
Do not allow bypassing the above settings: YES (applies to admins too)
Require linear history is the underrated one. It forces you to rebase your feature branch onto main before merging, rather than creating a merge commit. Your deploy history in Vercel then reads as a clean sequence of commits rather than a tangle of merge bubbles. When something breaks and you need to bisect the deploy log, you will care about this.
Preview deployments and UK customer feedback
Vercel generates a unique preview URL for every PR -- something like your-app-git-pr-42-yourname.vercel.app. This is free on all plans and is one of the best features Vercel ships.
For sharing with UK customers during a private beta:
Custom preview domain pattern using Cloudflare wildcard CNAME:
Add a wildcard CNAME in Cloudflare: *.preview.yourapp.co.uk pointing to cname.vercel-dns.com. Then in Vercel, set a wildcard custom domain for your project: *.preview.yourapp.co.uk. Vercel maps PR preview URLs to pr-42.preview.yourapp.co.uk automatically.
The result: you can share pr-42.preview.yourapp.co.uk with a customer rather than a URL containing your GitHub username and a commit hash. Looks professional. Costs nothing extra.
Basic auth for private betas:
Add two Vercel environment variables scoped to Preview:
BASIC_AUTH_USER=beta
BASIC_AUTH_PASSWORD=yourpassword
Then add middleware to enforce it:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (process.env.VERCEL_ENV === 'preview') {
const authHeader = request.headers.get('authorization')
const expected = `Basic ${Buffer.from(
`${process.env.BASIC_AUTH_USER}:${process.env.BASIC_AUTH_PASSWORD}`
).toString('base64')}`
if (authHeader !== expected) {
return new NextResponse('Authorisation required', {
status: 401,
headers: { 'WWW-Authenticate': 'Basic realm="Preview"' },
})
}
}
return NextResponse.next()
}
Customers get a browser username/password prompt. Simple, no extra dependencies.
Production deploy gate
The gate is simple: CI must pass before anything can merge to main. Vercel only deploys when it sees a new commit on main. No commit, no deploy.
Configure this in GitHub's branch protection as described above. The required status checks must include at minimum typecheck, lint, test, and build. If you add E2E or Lighthouse CI later, add those too.
The linear history requirement keeps the Vercel deployment log clean. Each production deploy maps to exactly one commit. When a deploy causes a regression, you know precisely which commit to roll back to.
Rollback in 30 seconds
Vercel's Instant Rollback is what it says. Open your project dashboard, go to Deployments, find the last good deployment, click the three-dot menu, select "Promote to Production". Done. No Actions step required, no git revert commit needed.
When to use it: a bug slips through CI (it happens) and reaches production. Roll back first, investigate second. Never write a hotfix under pressure when rollback is one click away.
Promote to Production is the companion feature. If you want to promote a specific preview deployment to production without merging to main -- say, a stakeholder approved a specific preview build -- you can do that from the same menu.
Neither of these requires any Actions configuration. They are Vercel dashboard operations. Do not build a rollback step into your Actions workflow unless you have a specific reason that Vercel's native rollback does not cover.
Cost ceiling for a UK indie hacker
| Tier | Cost | Actions minutes | Suitable for |
|---|---|---|---|
| GitHub Free | £0 | 2,000/month (public repos) | Open source projects |
| GitHub Pro | ~£3.50/month | 3,000/month | Private repos, most solo SaaS |
| Vercel Hobby | £0 | N/A | Non-commercial use only |
| Vercel Pro | ~£16/month | N/A | Any project with paying users |
The Vercel Hobby plan bans commercial use. The moment someone pays you -- even £1 -- you are in breach of the terms. Upgrade to Pro. At ~£16/month it is less than two cups of coffee in London, and it gives you preview deployments with custom domains, DDoS protection, analytics, and no commercial-use restriction.
A full CI run (typecheck + lint + unit tests + build) on a typical Next.js SaaS takes 3-5 minutes of wall-clock time, but Actions bills on minutes-per-job. With three parallel jobs averaging 2 minutes each, that is 6 billed minutes per PR. At 3,000 free minutes per month, you would need to merge 500 PRs before paying a penny. You will not hit that ceiling as a solo dev.
E2E with Playwright adds 3-8 minutes per run. Still well within free tier for a solo project.
DUA-compliant analytics gating
The Data (Use and Access) Act 2025 and the UK's post-Brexit PECR rules mean cookie consent is non-negotiable for a UK SaaS. The last thing you want is to copy a component from a tutorial, not notice it imports GA4, and ship an unconsented tracker to production.
Add a CI check that scans your app for known tracker imports:
cookie-audit:
name: Cookie compliance audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan for unconsented trackers
run: |
FLAGGED=$(grep -rn \
--include="*.tsx" --include="*.ts" \
-e "gtag\|GoogleAnalytics\|fbq\|_hsq\|hotjar\|clarity" \
app/ components/ lib/ || true)
if [ -n "$FLAGGED" ]; then
echo "ERROR: Potential unconsented tracker detected:"
echo "$FLAGGED"
exit 1
fi
echo "OK: No unconsented trackers found"
This allows: Plausible, Umami, Vercel Analytics (all cookieless by default).
This blocks: GA4 imports, Meta Pixel (fbq), HotJar, Microsoft Clarity -- unless you have explicitly wrapped them in a consent gate.
It is not a legal guarantee. It is a first-pass guardrail that catches the most common copy-paste mistakes before they hit production.
The seven things to actually run in CI
Full workflow snippets for each job you should be running.
1. Typecheck
- run: pnpm typecheck
Maps to "typecheck": "tsc --noEmit" in package.json. Catches the category of bugs that unit tests miss: wrong prop types, missing return types, API response shape mismatches.
2. Lint
- run: pnpm lint
Maps to "lint": "next lint" (ESLint with Next.js config). Add eslint-plugin-jsx-a11y and enforce it here -- catches accessibility violations before your UI designer sees the preview.
3. Unit tests
- run: pnpm test --coverage
Vitest or Jest, your choice. Coverage upload to Codecov is optional but free for open source and solo projects. A coverage badge on your README is a surprisingly effective trust signal for early customers.
4. Build smoke check
- run: pnpm build
Catches import errors, missing env vars that are referenced at build time, and broken dynamic imports. Runs in ~60-90 seconds on a typical App Router project.
5. E2E with Playwright (against Vercel preview URL)
e2e:
name: E2E tests
runs-on: ubuntu-latest
needs: [] # runs in parallel with other checks
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpx playwright install --with-deps chromium
- name: Wait for Vercel preview
id: vercel-preview
uses: patrickedqvist/wait-for-vercel-preview@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
max_timeout: 120
- name: Run E2E tests
run: pnpm test:e2e
env:
PLAYWRIGHT_BASE_URL: ${{ steps.vercel-preview.outputs.url }}
# Pin to London region for UK-accurate latency
PLAYWRIGHT_CHROMIUM_ARGS: "--use-gl=angle"
In your playwright.config.ts, set use.baseURL to process.env.PLAYWRIGHT_BASE_URL. Vercel preview deployments are served from the region nearest the runner by default -- add ?lhr1=1 as a query param or use Vercel's region-pinning header to force London for accurate UK performance data.
6. Lighthouse CI
lighthouse:
name: Lighthouse CI
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpx lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LIGHTHOUSE_CI_TOKEN }}
lighthouserc.js:
module.exports = {
ci: {
collect: { startServerCommand: 'pnpm start', url: ['http://localhost:3000'] },
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.85 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
},
},
upload: { target: 'lhci', serverBaseUrl: 'https://your-lhci-server.fly.dev' },
},
}
LCP under 2.5s and CLS under 0.1 are the UK-relevant Core Web Vitals thresholds for 2026. Google's ranking signal still weights these, and UK broadband speeds are highly variable outside London -- 85% performance score is a realistic floor, not a stretch target.
7. Bundle size delta as PR comment
Use next-bundle-analysis or github-action-bundle-size to post a PR comment showing how much the bundle grew or shrank. Catches the "accidentally imported the whole lodash library" class of mistakes early.
Five UK failure modes
1. Secrets exposed in Actions logs via PR forks
If your repo is public and a stranger opens a PR, Actions runs your workflow with access to repo secrets. Audit your workflow to ensure no step echoes a secret to stdout. Use ::add-mask:: for any dynamically constructed secret values. Never echo $MY_SECRET.
Mitigation: scope secrets to GitHub Environments and restrict the production environment to the main branch only.
2. Lighthouse running from a US datacentre
The default Actions runner (ubuntu-latest) is in the US East region. A Lighthouse run from there does not reflect your UK customers' experience. If you are self-hosting Lighthouse CI on a VPS, put it in a UK region (Hetzner Falkenstein, Fly.io LHR, or AWS eu-west-2). If you are using the LHCI GitHub app, accept that scores will be slightly optimistic for UK users and compensate with a stricter threshold.
3. E2E tests running against a US-region preview URL
Vercel preview deployments are served from the region nearest your Actions runner by default, which is US East. If your app has UK-specific latency concerns (Supabase in eu-west-2, Stripe webhooks, etc.), pin your Playwright tests to use the lhr1 (London) Vercel region. Add ?__vercel_edge=lhr1 to your base URL, or configure it via Vercel's regional routing if you are on Pro.
4. Hardcoded GBP in tests breaking when you add i18n
expect(price).toBe('£24.99') works until you add EUR support. Use currency-agnostic assertions from the start: expect(price).toMatch(/\d+\.\d{2}/) or test the raw numeric value rather than the formatted string. When it breaks it is a trivial fix, but it breaks at the worst possible time (during a late-night i18n push).
5. Missing .env.example causing first-PR failures
When your CI runs pnpm build and a required env var is not set, the build fails with an opaque error. Document every env var in .env.example with a safe placeholder value. Keep it in sync with .env.local. A CI step that diffs .env.example against the env vars referenced in your codebase is worth adding once your project grows beyond ten variables.
30-minute ship-it checklist
Run through this once when setting up a new Next.js SaaS project. It takes 30 minutes and saves hours of debugging later.
- Connect your GitHub repo to Vercel via the dashboard (not the CLI).
- Set all production secrets in Vercel environment variables (Stripe, Resend, Supabase service role).
- Create
.env.examplewith placeholder values for every env var. - Copy the CI workflow YAML from this post into
.github/workflows/ci.yml. - Add the
typecheck,lint,test, andbuildscripts topackage.json. - Push a test PR and verify all four CI jobs turn green.
- Enable branch protection on
main: require all four status checks, require linear history, no force push. - Add a Cloudflare wildcard CNAME for
*.preview.yourapp.co.ukand configure it in Vercel. - Add Lighthouse CI with LCP < 2.5s and CLS < 0.1 assertions.
- Add the cookie compliance audit step and verify it passes on your current codebase.
- Add basic auth middleware scoped to Vercel Preview environments.
- Run a test rollback from the Vercel dashboard to confirm you know how it works before you need it.
- Upgrade to Vercel Pro before you take your first payment.
Conclusion
The right pipeline for a UK indie hacker in 2026 is not complicated. Vercel handles deploys. GitHub Actions handles everything that should stop a bad deploy from reaching Vercel. Production secrets stay in Vercel. Branch protection keeps main honest. Lighthouse CI keeps performance honest. The cookie audit keeps regulators honest.
The temptation to build an elaborate custom pipeline grows as your product grows. Resist it until you have a team. For a solo dev, the overhead of maintaining a custom deploy step in Actions costs more than it saves. The setup above will serve you from zero to several thousand paying users without modification.
Want a practical breakdown of your next SaaS idea before you write a line of code?
IdeaStack publishes deep-dive market and technical reports for UK indie hackers -- real numbers, real competition, real risk. No fluff.
Frequently Asked Questions
Should I replace Vercel's native deploy with a GitHub Actions step?
No. For a solo UK indie hacker, Vercel's git integration is faster, cheaper, and more reliable than running 'vercel deploy --prod' from Actions. Use Actions for checks that gate the deploy, not for the deploy itself.
Which secrets go in GitHub Actions and which go in Vercel?
Production secrets (Stripe key, Resend API key, Supabase service role key) go in Vercel only. Actions secrets hold only CI tooling tokens: Codecov, Lighthouse CI, Cloudflare API token for DNS-only operations.
When should a UK indie hacker upgrade from Vercel Hobby to Pro?
The moment you have a single paying user. Vercel Hobby bans commercial use. Vercel Pro is roughly £16/month in 2026 -- treat it as a cost of doing business, not an upgrade.
How do I run E2E tests against the Vercel preview URL in GitHub Actions?
Wait for the Vercel deployment to complete (use the vercel-action output or poll the Vercel API), then run Playwright against the preview URL. Pin to the lhr1 region in your Playwright config to get UK-accurate latency.
Is the GitHub Actions free tier enough for a solo SaaS?
Yes. 2,000 minutes/month for public repos, 3,000 for GitHub Pro. A full CI run (typecheck + lint + test + build) on Node 22 typically takes 3-5 minutes. You would need to ship 600+ PRs a month to breach the free tier.
Topics





