Repository structure
crag-topo/
├── CLAUDE.md The working agreement. Read this first
├── pnpm-workspace.yaml Workspace packages + approved build scripts
├── .npmrc node-linker=hoisted, required by Metro
├── .nvmrc Node version, used by CI
│
├── .claude/skills/ Skills agents load for repeatable jobs
│ └── run-local/ running the app on the simulator, and its failure modes
│
├── apps/
│ ├── api/ @crag-topo/api — Cloudflare Worker
│ ├── mobile/ @crag-topo/mobile — Expo, iOS-first
│ └── docs/ @crag-topo/docs — Docusaurus renderer
│
├── packages/
│ └── shared/ @crag-topo/shared — the contract
│
├── docs/ The documentation itself, as MDX
└── .github/workflows/ CI and deployment
packages/shared
The contract between the app and the API. Consumed as TypeScript source — no build, no publish, no version skew.
src/
├── index.ts re-exports everything
├── domain.ts Crag, Sector, Photo, Route, RouteLine, Ascent, WishlistItem
├── grades.ts grade systems, route types, ascent styles, lossy normalisation
├── ids.ts prefixed ULID minting, slugs
├── sync.ts the sync protocol shapes and cursor encoding
├── api.ts the REST contract
├── globals.d.ts the Web Crypto surface, declared by hand
└── grades.test.ts
Two constraints, both easy to break by accident:
- It runs in three runtimes — workerd, Hermes (iOS), and Node (tests). No Node built-ins, no
DOM APIs.
globals.d.tsdeclares the little Web Crypto we use rather than pulling inlib.dom, which would let browser-only APIs typecheck and then fail on device. - Imports within the package are extensionless. Metro will not resolve
./grades.jstogrades.ts.
apps/api
src/
├── index.ts Hono app, middleware, error shape
├── env.ts Bindings and request-context types
├── db/schema.ts Drizzle schema — the source of truth for migrations
├── lib/
│ ├── errors.ts httpError() — one error shape everywhere
│ └── mappers.ts D1 rows <-> shared domain objects
├── middleware/
│ ├── context.ts request id + a Drizzle handle per request
│ └── auth.ts PLACEHOLDER — 501s in production
└── routes/
├── health.ts liveness + a real D1 round-trip
├── crags.ts browse, search, bundle
└── sync.ts pull and push
migrations/ generated by drizzle-kit, applied by wrangler
seed/seed.sql two real crags, re-runnable
test/api.test.ts runs in workerd against a real D1
wrangler.jsonc local / preview / production
worker-configuration.d.ts is generated by wrangler types and gitignored — the typecheck
script regenerates it.
apps/mobile
src/
├── app/ expo-router file routes
│ ├── _layout.tsx providers + native Stack
│ ├── (tabs)/
│ │ ├── _layout.tsx NativeTabs — real UITabBar
│ │ ├── index.tsx Crags
│ │ ├── logbook.tsx Logbook (placeholder)
│ │ └── profile.tsx Profile + sync diagnostics
│ ├── crag/[slug].tsx sectors within a crag
│ └── sector/[id].tsx routes within a sector
│
├── components/
│ ├── ui/ the domain-free kit — see ui-architecture.mdx
│ │ ├── index.ts the barrel everything imports from
│ │ ├── icons.ts the closed icon registry
│ │ ├── button · text-field · segmented-control · switch
│ │ ├── stack · screen · divider · list-row · tag · field
│ │ └── text · surface · glass · spinner · skeleton · empty-state
│ ├── grade-badge.tsx a grade, in its published system
│ ├── stars.tsx SF Symbols quality rating
│ └── topo-overlay.tsx route lines drawn on a photo
│
├── features/<area>/ per-area screens, components and data hooks
│ ├── <name>-screen.tsx pure design component
│ ├── use-<thing>.ts the only layer touching SQLite
│ └── *.stories.tsx every component has one; CI checks
│
├── lib/
│ ├── config.ts API URL, variant, build commit
│ ├── api.ts typed client; parses every response
│ ├── db.ts local SQLite schema + read helpers
│ └── sync.ts THE ONLY PLACE THAT TOUCHES THE NETWORK
│
└── constants/theme.ts palette, spacing, radii
app.config.ts is a TypeScript config, not app.json, because it reads environment variables —
that is how the PR preview bakes its API URL into the build.
apps/docs
The renderer, not the content.
docusaurus.config.ts points at ../../docs; onBrokenLinks: throw
sidebars.ts hand-written; adding a page means placing it
src/css/custom.css palette + the status pills used in tables
static/img/ logo, favicon
docs/
index.mdx start here
product/ overview, principles, roadmap, wishlist, mockups
architecture/ overview, data model, offline-first, API, iOS feel
architecture/decisions/ the ADRs and the decision table
engineering/ this section
wiki/ glossary, grading systems, prior art
.github/workflows
| Workflow | Trigger | Does |
|---|---|---|
ci.yml | push to main, PR | format, typecheck, test, docs build, migration apply |
preview.yml | PR (non-fork) | deploys the full preview stack, comments the links |
deploy.yml | push to main | migrations → Worker → verify → EAS Update → docs |
docs-check.yml | PR | comments if code changed but docs/ did not |
Details in CI/CD and previews.
Naming conventions
- Files: kebab-case.
glass-surface.tsx, notGlassSurface.tsx. - Packages:
@crag-topo/<name>. - Database:
snake_casetables and columns.camelCaseeverywhere in TypeScript. The translation happens once, inapps/api/src/lib/mappers.ts. - IDs:
<prefix>_<ULID>. Prefixes are inpackages/shared/src/ids.ts. - Migrations:
NNNN_readable_name.sql. Generated, then renamed by hand — and thetaginmigrations/meta/_journal.jsonrenamed to match.