Skip to main content

Getting started

Prerequisites

VersionNotes
Node22.xPinned in .nvmrc. nvm use
pnpm10.xcorepack enable
XcodeLatestiOS 18 simulator or newer. macOS only
A Cloudflare accountOnly for deploying. Local development uses Miniflare
An Expo accountOnly 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:

SymptomFix
xcodebuild refuses to runsudo xcodebuild -license accept
pod install fails on Unicode Normalization ... ASCII-8BITexport LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
Unable to find a destination matching..., iOS <version> is not installedxcodebuild -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.