| # CLAUDE.md | |
| Trailmark — an iOS app for logging hikes, backed by a Node.js REST API. | |
| - ios/ — SwiftUI app, iOS 17+, native components only | |
| - api/ — Node 22, TypeScript, Express, Postgres 16 (Kysely) | |
| - packages/contracts/ — zod schemas + generated OpenAPI. Single source of truth for both sides. Change the contract first, then the server, then the client. | |
| Nested rules live in api/CLAUDE.md and ios/CLAUDE.md. Longer workflows (release, migration, security review) are skills in .claude/skills/ — do not paste them here. | |
| ## Ask before you assume | |
| Never guess at intent. If a task leaves anything open — which screen, which endpoint, what happens on failure, whether it needs a migration, whether this is user-facing — stop and ask. One question up front is cheaper than half a day of work in the wrong direction. | |
| - Ask when the request could reasonably mean two different things. | |
| - Ask before changing a public API shape, a DB schema, or anything in packages/contracts/. | |
| - Do not invent product decisions, copy, or acceptance criteria. | |
| - Do not widen scope past what was asked. Note the adjacent thing you spotted; don't fix it unprompted. | |
| - If you had to assume something you couldn't resolve, list it explicitly at the top of your summary. | |
| ## The loop | |
| Every change runs through this. A task is not done until it is green. | |
| bash | | | npm run check # tsc --noEmit, eslint, prettier --check | | | npm test # vitest, unit + integration | | | npm run test:api # supertest against a throwaway Postgres | | | npm run db:migrate # never edit an applied migration, always add a new one | | | | |
| bash | | | cd ios && xcodegen && xcodebuild test -scheme Trailmark -destination 'platform=iOS Simulator,name=iPhone 17' | | | cd ios && swiftlint --strict | | | | |
| Rules for the loop: | |
| - Write the failing test first. Watch it fail for the right reason, then make it pass. | |
| - Run npm run check && npm test after every meaningful edit, not once at the end. | |
| - Never report success on a red loop. Never disable, skip, or .only a test to get to green. | |
| - If a test is wrong, say so and explain why before changing it. | |
| - Do not start long-running processes (npm run dev, expo start) to "verify" — they never exit. Use the commands above. | |
| ## Type checking | |
| Both languages are strict. Type errors are not warnings. | |
| tsconfig.json — these stay on: | |
| json | | | { | | | "compilerOptions": { | | | "strict": true, | | | "noImplicitAny": true, | | | "noUncheckedIndexedAccess": true, | | | "exactOptionalPropertyTypes": true, | | | "noImplicitOverride": true, | | | "verbatimModuleSyntax": true | | | } | | | } | | | | |
| - No any. If a type is genuinely unknown, use unknown and narrow it. | |
| - No as to escape an error, and no non-null !. Fix the type or narrow properly. | |
| - No @ts-expect-error without a comment naming the upstream issue. | |
| - All external input (request bodies, query params, API responses) is parsed with a zod schema from packages/contracts/. Types are inferred from the schema, never hand-written alongside it. | |
| - Swift: no force unwraps, no try!, no as!. Use guard let, typed throws, and Result at boundaries. | |
| Respect these from the start. Do not write loose code and correct it after the type check fails. | |
| ## Naming | |
| Consistency is for the model as much as for us. Pick the existing word, don't coin a new one. | |
| Domain vocabulary — one word per concept: | |
| | Concept | Use | Never | | |
| | --------------- | ---------------------- | ----------------------------- | | |
| | A recorded walk | hike | trip, walk, activity, session | | |
| | The GPS line | track | route, path, trace | | |
| | A saved place | waypoint | pin, marker, poi | | |
| | Account access | Sign in / Sign out | Login, Log In, Log out | | |
| Code: | |
| - Functions: createHike, getHike, listHikes, updateHike, deleteHike. Not fetch, remove, save, handle. | |
| - Booleans read as assertions: isSyncing, hasTrack, canEdit. | |
| - Routes: kebab-case, plural nouns — GET /v1/hikes/:hikeId/waypoints. | |
| - Postgres: snake_case tables and columns, plural tables — hikes, waypoints, started_at. | |
| - Swift types are UpperCamelCase and the file is named after the type: HikeDetailView.swift. | |
| - SwiftUI views end in View, observable state ends in Store: HikeListView, HikeStore. | |
| - Test files sit beside the source: hikes.service.ts → hikes.service.test.ts. | |
| User-facing copy: sentence case for buttons and labels ("Save hike", not "Save Hike"). Copy strings live in ios/Trailmark/Resources/Localizable.strings — no string literals in views. | |
| ## Project structure | |
| | | | api/ | | | src/ | | | modules/<domain>/ # route.ts, service.ts, repo.ts, *.test.ts | | | db/ # kysely client, generated types | | | middleware/ | | | lib/ # shared, dependency-free helpers | | | migrations/ # timestamped .sql, append-only | | | ios/ | | | Trailmark/ | | | Features/<Feature>/ # View, Store, and their tests | | | DesignSystem/ # Tokens.swift, reusable components | | | Networking/ # APIClient, generated contract types | | | Persistence/ # SwiftData models | | | packages/ | | | contracts/ # zod schemas, OpenAPI output | | | | |
| - New backend work goes in a module folder. Routes never talk to the DB directly — route → service → repo. | |
| - New screens go in Features/. Views hold no networking and no business logic; that belongs in the Store. | |
| - Nothing new at the repo root without asking. | |
| ## Dependencies | |
| Code is cheap; maintenance isn't. Prefer a well-established package over rolling your own, and prefer the platform over a package. | |
| Before installing anything, check and state: | |
| - Weekly downloads over ~100k, a release in the last six months, more than one maintainer. | |
| - Nothing single-maintainer or freshly published for anything touching auth, crypto, networking, or file I/O. | |
| - No new dependency for something the standard library, SwiftUI, or an existing dependency already does. | |
| Ask before adding a dependency. Never add one as a side effect of another task, and never pin to latest — exact versions only, lockfile committed. | |
| Native SwiftUI components only on iOS. No third-party UI frameworks. | |
| ## Performance | |
| Every endpoint has a p95 budget of 200ms. Anything slower is a bug, not a tuning opportunity for later. | |
| - Filter, sort, aggregate, and paginate in SQL. Never pull rows into Node to filter them there. | |
| - Every list endpoint is paginated: limit defaults to 25, hard maximum 100. No unbounded SELECT. | |
| - Select the columns you need. select * is not acceptable in a repo function. | |
| - No N+1. Join or batch — one query per request path, not one per row. | |
| - New query patterns come with an index in the same migration. Run EXPLAIN ANALYZE and include the plan in your summary if a query touches more than 10k rows. | |
| - Do a performance pass at the end of any feature that reads data, and say what you checked. | |
| On iOS: no work on the main actor beyond UI updates. Lists over 50 items use LazyVStack. Images are downsampled before display. | |
| ## Error handling | |
| Fail early, fail loudly, and never swallow. | |
| - No empty catch. No catch { console.log(e) }. Either handle it meaningfully or let it propagate. | |
| - Throw a typed AppError with a stable machine-readable code. Unexpected errors surface as 500 and page us — they do not get mapped to a friendly 200. | |
| - Every error response uses this envelope: | |
| ts | | | { error: { code: "hike_not_found", message: "That hike no longer exists.", details?: unknown } } | | | | |
| - Messages are useful to the client: what failed, and what to do about it. "Something went wrong" is not an error message. | |
| - Validation failures return 422 with the zod issue list in details. | |
| - iOS maps every error code to real copy and a real recovery action. No silent failures, no generic alert as a catch-all. | |
| - Log with structured context (hikeId, userId, requestId), never a bare string. | |
| ## End-to-end testing | |
| After any feature that spans both sides, exercise it like a person would — not just with unit tests. | |
| - Boot the API and the simulator, sign in, and drive the actual flow end to end: start a hike, record a track, background the app, foreground it, sync, confirm the row in Postgres. | |
| - Test the unhappy paths deliberately: airplane mode mid-sync, expired token, force-quit during a recording, duplicate submit. | |
| - Report what you clicked and what you saw. If a step failed, keep the failure — don't work around it and call it passing. | |
| You don't need to write Playwright suites for this. Driving the app as a human is the point. | |
| ## UI testing | |
| A green test suite tells you nothing about whether the screen looks right. | |
| - Screenshot every screen you touch, at least once, and look at it before you say it's done. | |
| - Check the small iPhone and the largest Dynamic Type setting. Check dark mode. | |
| - Use realistic seed data from api/seeds/realistic.sql — real place names, long names, empty states, a 400-item list. Never Lorem ipsum, never "Test User". | |
| - Look specifically for: clipped or truncated text, overlapping views, content under the safe area, missing empty state, missing state. | |
| ## Architecture | |
| Read this before exploring the codebase. If you find yourself searching for something that belongs here, add it (see below). | |
| Request path: SwiftUI view → Store → APIClient → Express route → service → repo → Postgres. Auth is a short-lived JWT in the Keychain with a refresh token; middleware/auth.ts populates req.user and every route below /v1 requires it. | |
| Sync is offline-first. The app writes to SwiftData immediately, queues a mutation, and reconciles on reconnect. Server timestamps win on conflict; the client never invents an id — it sends a client-generated idempotencyKey. | |
| Where things live: | |
| | You need | Look in | | |
| | -------------------------------- | ------------------------------------------- | | |
| | Auth, tokens, refresh | api/src/modules/auth/ | | |
| | Request/response schemas | packages/contracts/src/ | | |
| | DB client and generated types | api/src/db/ | | |
| | Migrations | api/migrations/ | | |
| | Colours, spacing, type scale | ios/Trailmark/DesignSystem/Tokens.swift | | |
| | Networking, retry, auth headers | ios/Trailmark/Networking/APIClient.swift | | |
| | Offline queue and reconciliation | ios/Trailmark/Persistence/SyncQueue.swift | | |
| | Env vars and their defaults | api/src/config.ts | | |
| | CI pipeline | .github/workflows/ci.yml | | |
| ## Keeping this file current | |
| This file is a failure log, not a wishlist. Every line below exists because it went wrong at least once. | |
| When you make a mistake, get corrected, or discover something about this codebase that wasn't written down: | |
| 1. Add one line to the failure log below, in the imperative, describing the correct behaviour. | |
| 2. Keep it specific to this repo. General advice belongs nowhere. | |
| 3. If the fix is a workflow rather than a rule, put it in .claude/skills/ and link it from here. | |
| 4. Include the change in the same commit and mention it in your summary. | |
| Keep this file under 500 lines. It is loaded into every session, and long context makes you less reliable, not more. If a section outgrows its usefulness, move it to api/CLAUDE.md, ios/CLAUDE.md, or a skill. | |
| ## Failure log | |
| - Do not run npm run dev to verify a change; it never exits. Use npm run check && npm test. | |
| - Run migrations against the test database before npm run test:api, or every test fails on a missing column. | |
| - xcodebuild needs xcodegen run first — the .xcodeproj is generated and gitignored. | |
| - Do not edit packages/contracts/dist/; it is generated by npm run contracts:build. | |
| - Do not add @MainActor to HikeStore methods that already run on the main actor — SwiftLint flags it and it hides real concurrency bugs. | |
| - Waypoint ordering comes from sequence, not created_at. Offline waypoints arrive out of order. | |
| - Never call deleteHike without checking for queued mutations against that hike first. |
Why I Chose PDF RAG Chunking and Metadata for Catalog Semantic Search