Current location is useful for “plan from here” and “find the nearest stop.” It is also sensitive data. The public app therefore keeps the assistant's location in the browser session rather than putting it into the assistant message sent to the API. That reduces one exposure, but it does not make every location-related action private. A map-selected point can be saved locally with a trip or placed into a share URL, and a typed place is sent to whichever configured place provider performs the search.

The system has two access surfaces. The rider app and its API are public. The operations dashboard is protected separately and talks to the collector through a service binding. A valid dashboard share token grants read access to the dashboard surface; it is not a rider-session credential.

Public location handling

This sequence answers one narrow question: where does a current-location coordinate go when the rider asks the assistant to plan from it?

The browser's wire body contains the message, UI language, optional conversation ID, and previous result type. The current implementation explicitly leaves GPS, place IDs, and full results out of that body. After the API returns an intent with usesCurrentLocation: true, the browser's deterministic operation reads its own session location and passes the resulting coordinate to the local planner.

The shared contract still validates coordinates at the request boundary:

example.ts
1export const geoPointSchema = z.strictObject({
2 lat: z.number().finite().min(-90).max(90),
3 lon: z.number().finite().min(-180).max(180),
4 accuracyMeters: z.number().finite().min(0).max(100_000).optional(),
5})

This validates shape, range, finiteness, and an optional accuracy value. It does not grant permission, decide whether the rider wants location sharing, or tell the rider how long the browser will retain a coordinate. The browser geolocation calls use a one-shot request with a 10-second timeout; the assistant session is in memory until it is reset or the page is unloaded.

Saved and shared trips have a different consequence. The activity schema permits endpoint coordinates, and the trip result code adds them only when the endpoint came from a map selection. A named-place share contains the two names. A map-selected share adds fromLng, fromLat, toLng, and toLat to the URL and asks the rider to confirm before sharing. Exact coordinates in a URL can survive in browser history, screenshots, copied messages, or another service's link preview. The warning appears before the share action; it cannot control those copies after the URL leaves the app.

Recent and saved trips are written to the local Dexie database named navigate-davao. The query persister uses the same database's kv table. The current code has no public API route for uploading saved trips. Clearing local data removes those Dexie tables and selected local-storage keys, then deletes the named transit bundle and manifest caches. It does not delete the Workbox precache containing the app shell and .wayplan, or the bus route tile cache. A future “erase everything” promise would need to cover those stores too.

Public API boundary

The API in apps/api/src/app.ts does not authenticate riders. It publishes transit and jeepney manifests and immutable versioned bundles, vehicle snapshots, intelligence, road intelligence, and bus route tiles. It applies CORS from an explicit origin allowlist, Hono security headers, and an in-process 120-request-per-minute limiter to most API paths. Bus route .pbf tile requests bypass that limiter because map rendering can issue many tile requests. This is request-abuse control, not user identity.

The assistant has a separate Cloudflare rate-limiter binding. The API checks both the request IP and an optional valid client key before inference. If the binding is absent or fails, the endpoint returns an unavailable response without calling the model. A configured limit returns 429. The body is capped at 16 KiB, the message field at 500 characters, and the inference path has a 12-second outer deadline. Assistant responses and failures use no-store because they contain request-specific intent or availability state.

The public boundary has focused tests. The API tests cover CORS, cache headers, and the in-process request limit; the assistant route tests cover disabled AI, missing or failing rate-limit bindings, the pre-inference 429 path, provider failure, malformed output, and the 12-second failure boundary. Browser tests cover legacy activity records, coordinate-bearing saved trips, and the distinction between named-place and map-selected share links. The dashboard authentication path does not have an equivalent test suite in the current repository.

Vehicle and intelligence responses have public cache policies because they are shared observations. The vehicle endpoint asks the collector service for a snapshot and service status, returns 502 when that call fails, and permits a 15-second cache with a 30-second stale-while-revalidate period. Public caching does not turn a vehicle coordinate into private user data, but it does mean that a reader may see a recent shared snapshot rather than the collector's newest state.

Dashboard authentication

The dashboard has a different request lifecycle. This sequence shows the Access-authenticated path.

The dashboard first checks the Cf-Access-Jwt-Assertion header with jose.jwtVerify. It obtains keys from ACCESS_TEAM_DOMAIN/cdn-cgi/access/certs, requires that domain as the issuer, and requires ACCESS_AUD as the audience. In a local Access context it can read the identity supplied by this.ctx.access. The admin check then compares the identity email with the ADMIN_EMAIL secret, case-insensitively.

Only three dashboard mutations are registered: POST /api/admin/start, /stop, and /collect. Each also requires x-davao-dashboard-action: 1, and rejects a request whose Sec-Fetch-Site value is present and not same-origin. A share-authenticated request has no email, so it can read the dashboard's GET handlers but cannot pass the admin check for these mutations.

The dashboard also supports an expiring read-only share credential. A request can present an access query parameter on a non-API URL or the davao_share cookie. The worker verifies an HS256 JWT with SHARE_SECRET, which must be at least 32 characters, and requires a numeric expiration claim. A valid query token is exchanged for an HttpOnly, Secure, SameSite=Lax cookie, the query string is removed with a 302 redirect, and the response is marked private and no-store. An invalid cookie is cleared.

The current repository contains the verifier but no token-issuance path. It also does not show a narrower scope claim for the share token. The safe conclusion is that a valid token grants read access to the dashboard handlers until expiry, while its creation, distribution, and intended audience remain an operational concern outside this source tree. A share URL still exposes the dashboard's read surface even though it cannot call the three admin mutations.

Browser and dashboard headers

The dashboard rewrites its bundled MapLibre CDN URLs to local assets, injects a per-response script nonce, and sends an enforced Content-Security-Policy with default-src 'none', same-origin scripts, local fonts, restricted image/connect origins, no objects, no framing, and disabled geolocation, microphone, and camera permissions. Its HTML and JSON responses are private and no-store.

The public web app has a different posture. Its public/_headers file sends Content-Security-Policy-Report-Only, allows geolocation for the site, and permits the map and place-provider origins needed by the rider app. Report-only policy records violations without blocking them. It should not be described as equivalent to the dashboard's enforced policy.

What the current source does not establish

No dashboard authentication test files are present under apps/dashboard; the dashboard package has typecheck and static UI scripts, but the Access, share-token, and header paths are not covered by the same unit tests that exercise the public API. The deployed Cloudflare Access policy is also outside this repository. Those are review and deployment boundaries, not evidence that the code path is unprotected.

The relevant sources are the public API entry point, the API rate limiter, the assistant wire client, the shared coordinate contract, the local-data clearing code, the activity store, the share-link builder, the public API tests, the assistant route tests, the dashboard worker, and the public web headers.