The main architectural choice is not a particular framework. It is deciding which work can happen with a verified local artifact and which work needs a network request. A rider should be able to plan from a route image already on the phone. A current vehicle position, a place-provider result, and an AI intent all depend on services that can be unavailable or stale.

This boundary map answers where those requests go. It does not describe every package.

The web app owns the rider-facing UI, MapLibre orchestration, local activity, query persistence, and the planner worker. The API is an HTTP boundary for public bundles, vehicle snapshots, intelligence, map tiles, and assistant intent extraction. The collector service owns the stateful observation path and exposes a service binding to the API and dashboard. The dashboard is a separate operator surface. It does not share the public app's access model.

Offline planning lifecycle

The planner request is deliberately local after initialization. The following sequence answers what happens when the browser prepares the planner and then receives a trip query.

The browser's fetch still goes through loadPlannerImage. The service worker can satisfy that fetch from its precache. The build configuration includes .wayplan and JSON assets in the Workbox glob, while API navigation is excluded from the app-shell fallback. The transport first reads planner-image.json, fetches the filename in that manifest, checks the byte length, and computes SHA-256 over the content portion. The worker repeats the digest check before calling mountPlannerImage.

The artifact is not a Dexie record. The current browser path uses Workbox and CacheStorage for the precached planner file. Dexie is used for a different purpose: PersistQueryClientProvider writes React Query state through the kv table, and the activity store writes recent and saved trips through the activity table. Confusing those stores would make a reset or corruption report misleading. In particular, clearLocalData clears activity, query state, selected local-storage keys, and two named data caches. It does not remove the Workbox precache or the bus tile cache.

The shared client also has a product-level concurrency rule. It keeps one pending plan request. When a newer request arrives, the older promise is rejected as superseded. The worker can therefore spend time on the newest rider intent without allowing an old result to replace it in the UI. A fetch, digest, mount, or worker failure becomes planner-unavailable; it does not turn into an invented trip.

The current browser evidence is in the Workbox configuration, the image transport, the shared planner client, and the worker entry point.

Online public lifecycle

The tracker follows a different path because its answer is a current snapshot rather than static topology. This sequence answers how a browser request reaches the collector.

The API calls the collector through a Cloudflare service binding. The collector service delegates the read to the named VehicleCollector Durable Object and reports a failure when no snapshot is available. The public vehicle response is cacheable for 15 seconds with a 30-second stale-while-revalidate window. The browser refreshes the query every 20 seconds, so “live” here means a periodically refreshed observation, not a continuous connection.

The assistant is online at its language boundary, but not at its routing boundary. The browser posts the message, UI language, optional conversation ID, and previous result type to /api/ai/chat. The API asks the configured model for one structured intent, validates the result, and returns that intent. The browser then dispatches the action locally or calls the public vehicle endpoint. The model does not receive the mounted graph and does not write transit facts into the response.

Failure boundaries

These failures have different meanings:

  • A missing or corrupt .wayplan prevents local planning until a verified artifact is available.
  • A missing route bundle can leave route pages unavailable; the bundle loader can try the last good version stored in local storage.
  • A collector failure makes vehicle or intelligence requests unavailable, while static route data can remain usable.
  • A disabled, rate-limited, timed-out, or malformed AI request returns an unavailable or unknown result with Cache-Control: no-store.
  • Dashboard access failure does not make the public API private, and public API availability does not grant dashboard access.

The public routing and data boundary is defined in apps/api/src/app.ts. The collector service and its Durable Object bindings are in packages/collector/src/index.ts and its Wrangler configuration.