Tracking a fire brigade's UTV on the incident map
Context
During a person search, a volunteer fire brigade sends a utility terrain vehicle (UTV) into the area. The crew takes an iPad with them; the command post needs to know where the vehicle has already searched, without asking for a position over radio every few minutes.
The incident map they use, lagekarte.info, can import GeoJSON. What was missing was the piece in between: a service that receives the iPad's GPS feed, keeps a history, and hands back a route for a chosen time window.
What it does
- Receives positions from the Traccar client on the iPad, including Traccar Server's JSON forwarding format.
- Stores position history per device in DynamoDB, with a 90-day TTL and 180-day device records.
- Lets an invited user inspect a route on a Leaflet map and export it as GeoJSON points or a line.
- Mints a public, expiring share link that lagekarte.info can fetch without an account.
Constraints
- A field device, intermittent connectivity and no training session before an operation.
- Position data is operationally sensitive: a route reveals where people have already searched.
- Public map consumers cannot sign in, but must not be able to enumerate devices or widen a shared window.
- Ingestion has to stay anonymous for the Traccar client, so the public endpoint needs abuse controls.
Decisions
The backend is a single Go Lambda behind API Gateway, with a DynamoDB single-table design: one partition per device, a sort key ordered by UTC timestamp, and idempotent writes for retransmitted fixes. The frontend is Svelte 5 with TypeScript and Leaflet; the map loads GeoJSON points and a derived LineString. Everything is deployed with AWS CDK, with separate staging and production stacks.
Sharing was the interesting part. The first version exposed device-ID URLs, which made the device guessable and the time window editable. I replaced that with a bearer link: 32 random bytes, only the SHA-256 hash stored, fixed bounds captured at creation, a seven-day TTL and a revoke action. The public URL contains the token, never the device ID. Expired, revoked and unknown tokens all return 404, so the endpoint cannot be used as an oracle.
Security and operations
- Invitation-only Cognito login with authorization code + PKCE; ID tokens stay in memory, not in local storage.
- Separate throttles for anonymous ingestion and public share reads; request bodies are capped at 64 KiB.
- Route responses are capped at 50,000 points and roughly 5 MiB, failing with 422 instead of truncating a route.
- Access logs omit the request path because share tokens live there; raw tokens never reach DynamoDB or the logs.
- Staging deploys from
main; production only from a version tag, with smoke tests after each deploy.
The checks run the same way locally and in CI: Go unit tests and DynamoDB integration tests, Svelte and TypeScript checks, Playwright end-to-end tests, CDK assertions and a lint pass. Renovate keeps the dependencies moving; nothing merges or deploys without the checks.
Outcome
The service is deployed and the brigade can turn a recorded track into a layer on the incident map. The share link is what made it usable in an operation: the command post needs no account, the URL exposes no device ID, and the link expires on its own.
It also became a realistic playground for the things I care about in production: infrastructure as code, privacy boundaries, failure modes and tests that behave the same on a laptop and in CI.
What I would do differently
I would design the public sharing boundary before the first deployment. The initial device-ID URLs were convenient, but they turned the security model into a later migration that touched the API, the frontend, the CDK stack and the documentation. Starting from “the public consumer gets a capability, not an identifier” would have saved that work.