Skip to main content

ADR-0006 — A full preview stack on every pull request

Date2026-08-17
Superseded by

Context

The stated requirement:

When things are pushed or created as a PR, I want to deploy all these and add them to the PR so that I can test them from my phone really easily without having to do local connections and all that stuff.

The thing being avoided is specific and familiar: to try a branch on a real phone you normally start Metro, find your laptop's LAN address, make sure the phone is on the same network, point the app at a local API, and discover the tunnel has died. It is five minutes of friction on every iteration, and the result is that changes get tested in a simulator instead of on a phone — which for an app used outdoors in bright sun with cold hands is the wrong place.

Decision

Every pull request deploys a complete, isolated stack and posts the links back as a sticky comment.

ComponentHowIsolation
APIwrangler versions upload --env previewIts own version preview URL; production traffic untouched
Databasewrangler d1 migrations apply --env preview --remoteShared preview D1, migrated
iOS appeas update --branch pr-<n>Its own EAS branch
Docswrangler pages deploy --branch pr-<n>Its own Pages preview URL

The detail that makes it work:

The app update has the PR's API URL baked into it. EXPO_PUBLIC_API_URL is set from the Worker preview URL captured earlier in the same job, so scanning the QR code gives you a phone already pointed at the backend built from the same commit. No local setup, no editing a URL.

Two more deliberate choices:

  • versions upload, not deploy. It publishes a version and returns a preview URL without shifting any production traffic. There is no way for a preview to become production by accident.
  • The workflow polls /health and asserts the returned revision matches the PR head SHA before commenting. A green comment therefore means the preview is genuinely up and serving the right commit, not that a deploy command exited zero.
  • Forked PRs get CI only. They cannot read secrets, and deploying them would let anyone with a fork write to the preview database.

Consequences

What gets harder:

  • The preview database is shared across PRs. Two branches with conflicting migrations will fight. Per-PR D1 databases are possible (wrangler d1 create per PR) and were rejected for now as more moving parts than the current volume of work justifies. If it becomes a problem, that is the fix.
  • Migrations run against a real database before review. A destructive migration on a preview branch damages the preview data. Preview data is disposable, but the seed script has to be re-runnable — it is.
  • Secrets are needed for the pipeline to work at all. CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, EXPO_TOKEN, plus the EAS_PROJECT_ID variable. Until those exist, the preview workflow fails on every PR. Listed in Environments.
  • Third-party actionsexpo/expo-github-action and marocchino/sticky-pull-request-comment — are in the supply chain of a workflow that holds deploy credentials. They are pinned by major tag; pinning to a SHA would be stricter.

What we are committed to:

  • Cloudflare and EAS as the deploy path. The workflow is written around wrangler and eas specifically.
  • Keeping /health honest, because the pipeline asserts on it.

What this creates:

  • A development build has to exist on the phone before any of this is useful. Expo Go cannot load EAS Updates. This was not obvious when the decision was made and is the single biggest prerequisite: a build signed with a free Apple ID lasts seven days, so the working pattern is a couple of minutes at the Mac each week, after which every PR is a scan from anywhere. Paying for the Apple Developer Program (£79/yr) removes the weekly rebuild and allows cloud builds; it is not needed to review a PR. Details in CI/CD.
  • Every PR costs a Worker version, an EAS update, and a Pages deployment. All cheap; not free.
  • A rule worth stating: do not merge a PR whose preview never came up. A broken preview means either the change or the pipeline is broken, and both are worth knowing before merge.

Alternatives considered

Expo Go with a tunnel, run locally

The default workflow. Rejected — it is exactly the friction the requirement names. It also cannot be shared: nobody else can try your branch.

TestFlight builds per PR

Real builds, real distribution. Rejected on cycle time. Even with internal testing, a build and upload is minutes-to-tens-of-minutes against seconds for an EAS Update, and it burns build credits. Full builds still matter for release candidates and for anything that changes native code — an EAS Update cannot deliver a new native module.

API preview only, app pointed at it manually

Half the work. Rejected because the manual half is the annoying half. Typing a workers.dev URL into a settings screen on a phone, per PR, is precisely the friction being removed.

A single shared staging environment

Simpler, one URL. Rejected because branches then queue behind each other, and "is staging currently running my branch?" becomes a question people have to ask.

Open questions

  • Per-PR databases. Worth doing when two PRs with schema changes are open often enough to collide.
  • Cleanup. EAS branches for merged PRs are not deleted. Neither are Worker versions. Both accumulate.
  • Native builds. An EAS Update cannot deliver a native module change. There is no workflow yet for "this PR needs a new dev build" — currently it has to be said in the PR description.
  • Pinning actions to SHAs rather than major tags.