The starting point for a new React Native app: an Expo project with the signing, the pipelines, the emulators and the store submissions already working.
Badges are per branch — these are main's. See CI.
Every React Native project pays the same tax before it ships anything. Signing that only fails on a Tuesday. An emulator that hangs in CI and nowhere else. The eleventh linter. A release nobody remembers the order of. Months of it, and none of it is the app itself.
That tax is paid here once. Press Use this template, run make init, and
the result is an app that builds, tests itself on real devices, and ships to
both stores from a merged pull request.
flowchart LR
push[push] --> checks[Checks] --> unit[Unit] --> e2e[E2E on device]
checks --> sec[Security scans]
e2e --> merge[merge to main]
merge --> relpr[Release PR] --> build[Signed builds] --> stores[TestFlight and Play]
Where to start. Three ways through this repository:
- Starting a new app — Getting started to run it, then
Using this template for what
make initrewrites and local-dev.md for the long version. - Working in the app — What is in here maps every directory, architecture.md covers data flow, providers and env, quality.md says which linter owns which rule.
- Shipping it — The pipelines lists every workflow and job, release-runbook.md is how to cut, promote, roll out and halt, store-accounts.md is the accounts and credentials.
make setup # blank machine → ready: toolchain, deps, Maestro, Android SDK + emulator, iOS
make doctor # verify it (one line per tool)
make dev-api # terminal 1: GraphQL mock API
make dev # terminal 2: Metro for the dev client
make dev-ios # or: make dev-androidmake setup is idempotent (re-run it when the toolchain misbehaves) and asks
before accepting the Android SDK licences; ARGS=--yes agrees up front, for CI.
The pitfalls it guards against, symptom by symptom, are in
.claude/skills/native-setup.
make check && make test-unit runs every gate CI runs. make help lists the
rest, grouped by prefix: check- gates, test- tests, build- artifacts,
dev- the app locally, gen- generated files, fix- rewrites in place,
verify- inspects a build.
If a command is not in the Makefile, CI does not know how to run it either. That is the rule the whole repo is built on, and
make ciruns the whole of CI locally.
Press "Use this template" on GitHub, then run make init: it renames the
project, optionally drops the web target, and deletes itself. The full walkthrough
— what it asks, what it rewrites and what to do afterwards — is in
docs/template-usage.md.
Continuous Native Generation. Native config is a TypeScript plugin, not a diff
someone applied to an Xcode project two years ago and cannot explain.
make prebuild regenerates both. Neither is committed.
The workflows live in
blinkbitcoin/shared-workflows,
pinned here to one commit that Dependabot moves. What is left in this repo is eleven short files naming
which ones to run. A fix to the Android emulator boot lands once, for every app
in the family.
CI runs make. So a red build is reproducible from the job name, and a new
gate is one Makefile line instead of a YAML negotiation.
Builds go unsigned. Signing switches on with a repo variable, uploading with another. A release can be watched end to end before the store accounts exist.
| Path | Responsibility |
|---|---|
src/app/ |
expo-router routes. A file here is a screen; nothing else is |
src/components/ |
The themed component kit, each with its own test |
src/features/ |
Feature modules — the screens' actual logic, kept out of the route files |
src/graphql/ |
Queries and mutations, plus the typed documents codegen writes from them |
src/config/ |
zod-parsed EXPO_PUBLIC_* env. Nothing reads process.env directly |
src/i18n/ |
Lingui setup and the en and es catalogs |
src/theme/ |
Design tokens, colours and typography |
src/lib/ |
Shared helpers that belong to no single feature |
src/services/ |
The Apollo client and its retry, auth and error links |
modules/ |
A local Expo native module (hello-native) — the worked example of native code |
plugins/ |
Config plugins. with-build-stamp.ts shows the pattern: native config as TypeScript |
fastlane/ |
Fastfile plus one lane file per platform, store metadata, Matchfile for signing |
scripts/ |
Every gate and helper make calls, with node:test files next to them |
mocks/ |
The GraphQL mock API — one schema, served to Jest via MSW and to E2E as a server |
.maestro/, e2e/ |
Maestro flows for device E2E, Playwright specs for web |
assets/ |
Icons, splash screens and fonts |
certs/ |
The public OTA code-signing certificate. Never a private key |
deploy/ |
Deployment for the self-hosted update server |
docs/ |
Twelve pages, indexed by question in docs/README.md |
Not in here, deliberately: ios/ and android/. They are generated.
Eleven workflow files. Each is a thin caller —
shared-workflows
holds what they actually do. Job names are what the Actions graph shows.
CI — on a pull request and on main
| Workflow | Jobs | Fires on |
|---|---|---|
ci.yml |
ChecksUnitE2ESecurityBadges |
Push, PR, dispatch. Each job gates the next, so a failed unit run never reaches E2E; E2E skips a change it cannot affect; Security runs the scanners beside them |
ci-codeql.yml |
Analyze |
Push, PR, weekly. Informational, never a required check |
ci-web.yml |
Web |
PR and release — the web export and Playwright suite, skipped on a PR it cannot affect |
ci-pr-title.yml |
Title |
Conventional Commits on the PR title |
ci-pr-closed.yml |
Cleanup |
Cancels the closed PR's runs, drops its badges |
CD — on a merge, a release, or a deliberate dispatch
| Workflow | Jobs | Fires on |
|---|---|---|
cd-release.yml |
ReleaseRelease notes |
Push to main. Maintains the version PR and drafts the store notes into it; dispatches beta and web at a cut release |
cd-internal.yml |
PrepareBuild iOSBuild AndroidUpload iOSUpload AndroidPre-releaseOTA |
Push to main, once CI is green for that sha. TestFlight and the Play internal track |
cd-beta.yml |
PreparePromote iOSPromote AndroidReleaseStage release notesAttach release notesOTA |
Dispatched by cd-release.yml at the tag. Promotes the internal build rather than rebuilding |
cd-production.yml |
PrepareSecurityRelease iOSRelease AndroidPhased iOSRollout AndroidHalt AndroidReleaseOTAWeb |
Dispatch only, carrying the action. Security checks the release's binaries first;phased release and staged rollout, with a halt |
cd-beta-retry.yml |
Retry Beta |
A failed beta run. Retries it without a human |
cd-ota-hotfix.yml |
Fingerprint baseline · Publish |
Dispatch. Ships JS without a store round trip, gated on the native fingerprint |
Merge a pull request and release-please opens the version PR. Merge that, and the tag, the signed builds, the artifact verification, the GitHub release and the TestFlight and Play submissions all happen without anyone typing a command. Beta promotion and the staged production rollout are one dispatch each, and a bad rollout is halted the same way.
Release runbook is the whole path, including how to rehearse it. Store accounts covers getting the accounts and credentials in the first place — Apple, Google, Huawei, Samsung.
docs/README.md says which page answers which question. Two to
start with: local-dev to get the app running,
architecture for how it is laid out. AGENTS.md is
the rules-of-the-road file, for humans and coding agents alike.
MIT — see LICENSE.