Notes for working in this repo, aimed at someone newer to the Next.js + Turborepo stack. Read alongside the PRD, ADRs, and TASKS file.
- Install Node ≥ 20.11 and pnpm ≥ 9 (
corepack enable && corepack prepare pnpm@9.12.0 --activate). pnpm installfrom the repo root.- Copy
apps/web/.env.exampletoapps/web/.env.local(note the path — Next.js reads this file relative to the web app, not the repo root) and fill in:ORS_API_KEYfrom https://openrouteservice.org/dev/#/signup (verify the signup email — keys don't work until verified)GEOAPIFY_API_KEYfrom https://myprojects.geoapify.com/NEXT_PUBLIC_STADIA_API_KEYfrom https://client.stadiamaps.com/
pnpm devand open http://localhost:3000.
A .env.local at the repo root will be silently ignored by Next. If you see "X not set" errors, that's the first thing to check.
| Command | What it does |
|---|---|
pnpm dev |
Starts the Next.js app in watch mode. |
pnpm typecheck |
Runs tsc --noEmit across all packages via Turborepo. |
pnpm lint |
ESLint across all packages. Zero warnings tolerated. |
pnpm test |
Vitest unit tests (currently in packages/providers). |
pnpm test:e2e |
Playwright smoke tests (lands in T-13). |
pnpm format |
Format all source with Prettier. |
Turborepo caches everything; the second run of pnpm typecheck should be instant.
apps/
web/ Next.js 14 App Router app
assets/graphs/ Committed walk-graph binary asset (built by tools/graph-pipeline)
src/
app/ Pages and route handlers
components/ UI components (map/, controls/)
lib/ Hooks, helpers, config
packages/
engine/ Pure-TS isochrone engine (asset parsing, snap, Dijkstra, polygonize)
providers/ Provider abstraction (isochrone, poi, tile, transit)
src/
isochrone/ IsochroneProvider + ORS/local adapters
poi/ PoiProvider + Geoapify adapter
tile/ TileProvider + Stadia adapter
transit/ TransitDataProvider (phase 2 only)
tools/
graph-pipeline/ Python (uv) build pipeline: OSM extract -> walk-graph asset
docs/ PRD, ADRs, TASKS, research, this file
The isochrone engine consumes a binary walk-graph asset committed at
apps/web/assets/graphs/walk-tlv.v1.bin, built by tools/graph-pipeline
(Python, managed by uv - not part of the pnpm
workspace). Rebuild it when map data should refresh:
cd tools/graph-pipeline
uv sync # one-time: creates .venv with Python 3.12
uv run build-graph # downloads the Geofabrik Israel extract (cached), writes the asset
uv run pytest # offline pipeline tests on the committed tiny fixture
uv run build-graph --fixture # regenerate packages/engine/src/__fixtures__/tiny-walk.v1.binCommit the regenerated asset. The binary format is a cross-language contract -
see docs/reference/graph-asset-format.md before touching it. Engine/provider
selection is ISOCHRONE_PROVIDER=local|ors (see apps/web/.env.example).
UI component ─► SWR hook ─► /api/* route handler ─► Provider adapter ─► External API
(client only) (client) (server only) (server only) (HTTPS)
Three rules to keep yourself out of trouble:
- Adapters never run in the browser. They live in
packages/providers/src/{isochrone,poi}/*.tsand are only imported from route handlers. The route handler hides API keys; the adapter implements the upstream contract. - UI components don't know the provider. They consume
Poi[]and GeoJSON polygons. Switching ORS to OTP is a route-handler change, not a UI change. - All provider responses go through Zod parsing in the adapter. That's where typed safety begins. Anything past the adapter boundary is fully typed.
- New isochrone provider → new file in
packages/providers/src/isochrone/, implementIsochroneProvider, swap inapps/web/src/app/api/isochrone/route.ts. - New POI category → extend
PoiCategorySchemainpackages/providers/src/types.ts, add the mapping in each POI adapter, add the toggle button inCategoryToggles.tsx(T-09). - New URL state field → extend
AppUrlStateinapps/web/src/lib/url-state.ts(parser + serializer + default).
- TypeScript strict +
noUncheckedIndexedAccess. Array and Record lookups returnT | undefined. This catches off-by-one and missing-key bugs early. When you see aCannot read properties of undefinedin production, it's because someone disabled this somewhere. 'use client'is a runtime boundary, not a file-type boundary. A file marked'use client'and everything it imports is bundled for the browser. The map component is dynamically imported withssr: falsebecause MapLibre toucheswindowat module load.- Imports are extensionless inside the providers package. TS with
moduleResolution: "Bundler"resolves./typesto./types.ts. We don't use the.jsESM extension because Next.js's webpack treats it literally and fails to resolve workspace-package files. If we ever publish providers as a standalone Node ESM package, we'd add a build step that rewrites these. - No emojis in code or commit messages. No bullet-point essays in PR descriptions either; one paragraph per change.
This project is set up for AI-assisted development. The recommended loop:
- Pick the next task from
docs/TASKS.md. - Open Claude Code (or your agent of choice) in the repo root.
- Paste the task entry as the prompt. The PRD and ADRs are already on disk, so the agent has full context.
- Let the agent plan, edit, and run
pnpm typecheck && pnpm test. - Open a PR. A reviewer subagent should be invoked manually with a checklist:
- Does the change match the PRD? (Cite the section.)
- Are there new provider-specific types leaking past adapter boundaries?
- Is attribution still correct?
- Did unit tests cover the new behavior?
- Merge when CI is green and the reviewer's punchlist is clean.
When the PRD and the code disagree, fix the PRD first (commit the diff), then change the code. That keeps the spec honest.