Getting started
Prerequisites
| Version | Notes | |
|---|---|---|
| Node | 22.x | Pinned in .nvmrc. nvm use |
| pnpm | 10.x | corepack enable |
| Xcode | Latest | iOS 18 simulator or newer. macOS only |
| A Cloudflare account | — | Only for deploying. Local development uses Miniflare |
| An Expo account | — | Only for EAS builds and updates |
You do not need a Cloudflare or Expo account to run everything locally.
First run
git clone https://github.com/mattmoran56/crag-topo.git
cd crag-topo
pnpm install
Then bring the database up and load real seed data — two crags, five routes, a topo photo with two route lines drawn on it:
pnpm api:migrate:local
pnpm api:seed:local
Start the API:
pnpm api:dev # http://localhost:8787
Check it:
curl -s http://localhost:8787/health
curl -s http://localhost:8787/v1/crags | head -c 400
curl -s http://localhost:8787/v1/crags/stanage-popular/bundle | head -c 400
In a second terminal, start the app:
pnpm mobile:ios # opens the iOS simulator
And the docs site:
pnpm docs:dev # http://localhost:3000
Running the app: first-run prerequisites
The first iOS build on a new machine needs three things that are easy to miss, each of which fails with an error that does not obviously name the cause:
| Symptom | Fix |
|---|---|
xcodebuild refuses to run | sudo xcodebuild -license accept |
pod install fails on Unicode Normalization ... ASCII-8BIT | export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 |
Unable to find a destination matching..., iOS <version> is not installed | xcodebuild -downloadPlatform iOS — several GB |
The third one bites after an Xcode upgrade: Xcode ships the SDK but not the matching simulator runtime, so there is no valid build destination at all — not for the simulator, not for a device.
:::tip There is a skill for this
.claude/skills/run-local/ captures the whole loop, including how to screenshot the simulator,
how to read JS errors out of the device log, and what to check when the app builds but shows an
endless splash screen or an empty crag list.
:::
Checks before you push
pnpm typecheck # every package
pnpm test # every package
pnpm format:check # prettier
CI runs all three plus a docs build (which fails on a broken link) and a migration apply.
Working on the API
pnpm --filter @crag-topo/api dev # Wrangler dev server
pnpm --filter @crag-topo/api test # vitest inside workerd, real D1
pnpm --filter @crag-topo/api typecheck # regenerates Cloudflare types first
Talking to the sync endpoints locally
There are no accounts (ADR-0007) — the bearer token is just a user id. Use the one from the seed data:
curl -s http://localhost:8787/v1/sync/pull \
-H 'content-type: application/json' \
-H 'authorization: Bearer usr_01HZZZZZZZZZZZZZZZZZZZZZ01' \
-d '{}' | head -c 600
No secret, no .dev.vars, nothing to set up.
:::warning This is identity, not authentication
The server takes the id at face value. In production the sync endpoints return 501 unless
ALLOW_UNAUTHENTICATED is set, so this cannot reach real users by accident.
:::
Changing the schema
Never hand-write a migration:
# 1. edit apps/api/src/db/schema.ts
pnpm --filter @crag-topo/api db:generate
# 2. rename migrations/0001_<random_words>.sql to something readable,
# and update the matching `tag` in migrations/meta/_journal.json
# 3. update packages/shared/src/domain.ts
# 4. update the local mirror in apps/mobile/src/lib/db.ts
# 5. update docs/architecture/data-model.mdx
Step 5 is not optional — see
CLAUDE.md.
Resetting the local database
rm -rf apps/api/.wrangler
pnpm api:migrate:local && pnpm api:seed:local
Working on the app
pnpm --filter @crag-topo/mobile start # Metro
pnpm --filter @crag-topo/mobile ios # + iOS simulator
pnpm --filter @crag-topo/mobile typecheck
pnpm --filter @crag-topo/mobile deps:check # native module versions match the SDK
pnpm --filter @crag-topo/mobile doctor # expo-doctor
Pointing the app at something other than localhost
EXPO_PUBLIC_API_URL=https://some-preview.workers.dev pnpm --filter @crag-topo/mobile start
The Profile tab shows which API the running build is using, which saves a lot of confusion.
Adding a dependency
pnpm --filter @crag-topo/mobile exec expo install <package>
expo install picks the version that matches the SDK. pnpm add does not, and mismatched native
modules fail in ways that are hard to read.
A new native module needs a new dev build, not just a reload. Say so in the PR description.
Liquid Glass in the simulator
Requires a recent-enough simulator. GlassSurface falls back to a solid card when
isLiquidGlassAvailable() is false, so an older simulator will render correctly but plainly —
that is the fallback working, not a bug.
Working on the docs
pnpm docs:dev # hot reload on http://localhost:3000
pnpm docs:build # what CI runs; fails on broken links
Content is in docs/ at the repo root, not in apps/docs. Adding a page means adding it to
apps/docs/sidebars.ts — sidebars are hand-written on purpose.
See Documentation standards before writing.
Common problems
Metro cannot resolve @crag-topo/shared. Check apps/mobile/metro.config.js still has
watchFolders and nodeModulesPaths, then pnpm install from the repo root.
wrangler types fails or types look stale. It runs as part of pnpm --filter @crag-topo/api typecheck. Run it directly with pnpm --filter @crag-topo/api cf-typegen. The generated
worker-configuration.d.ts is gitignored.
API tests fail with a workerd error. Approve the build scripts pnpm blocked:
pnpm approve-builds, or check the onlyBuiltDependencies list in pnpm-workspace.yaml.
The app shows an empty crag list. Expected on a fresh install — nothing has synced. The app reads local SQLite only. Tap "Sync now" on the Profile tab with the API running.