Skip to main content

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.ts declares the little Web Crypto we use rather than pulling in lib.dom, which would let browser-only APIs typecheck and then fail on device.
  • Imports within the package are extensionless. Metro will not resolve ./grades.js to grades.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

WorkflowTriggerDoes
ci.ymlpush to main, PRformat, typecheck, test, docs build, migration apply
preview.ymlPR (non-fork)deploys the full preview stack, comments the links
deploy.ymlpush to mainmigrations → Worker → verify → EAS Update → docs
docs-check.ymlPRcomments if code changed but docs/ did not

Details in CI/CD and previews.

Naming conventions

  • Files: kebab-case. glass-surface.tsx, not GlassSurface.tsx.
  • Packages: @crag-topo/<name>.
  • Database: snake_case tables and columns. camelCase everywhere in TypeScript. The translation happens once, in apps/api/src/lib/mappers.ts.
  • IDs: <prefix>_<ULID>. Prefixes are in packages/shared/src/ids.ts.
  • Migrations: NNNN_readable_name.sql. Generated, then renamed by hand — and the tag in migrations/meta/_journal.json renamed to match.