How it's built

This site is a small production system: a full-screen 3D map backed by an edge API, a database and a rate limiter. Here is how the pieces fit and why they were chosen.

Architecture

Architecture diagramThe browser loads the app from Cloudflare Pages and calls its edge API routes, which read and write Cloudflare D1 and use Cloudflare KV for rate limiting. The browser also fetches CARTO map tiles and calls Nominatim and Photon directly for geocoding.Cloudflare Pages · edge runtimeHTTPSBrowserNext.js · MapLibre · deck.glCARTO tilesvector basemapNominatimreverse geocodingPhotoncity searchStatic assetsHTML · JS · images/api/visitorsGET · POST · DELETE/api/ip-locationrequest.cf cityCloudflare D1city · country · countCloudflare KVrate-limit counters
Solid arrows go through Cloudflare; dashed boxes are third-party services called straight from the browser.

Decisions and trade-offs

Privacy by design

The map needs a city, not a person.

  • Only city, country and city-centre coordinates are stored — no IP, user agent or identifiers.
  • Exact GPS is used once for reverse geocoding in the browser and discarded.
  • Stored coordinates are rounded to ~1 km so a row cannot pinpoint a visitor.
  • Consent is required before the location prompt is enabled.

Geolocation resilience

Real devices fail in many ways; every failure has a next step.

  • Transient errors are retried, then a high-accuracy attempt is made.
  • A watchdog turns ignored permission prompts into a timeout instead of hanging.
  • If GPS fails (but was not denied), Cloudflare's city-level estimate is offered as a suggestion only.
  • Manual city search is always available as the last resort.

Security hardening

A small public API still gets abused.

  • Strict Content-Security-Policy scoped to the origins the app talks to.
  • Per-IP rate limiting in Cloudflare KV: POST 3/10 min, GET 60/min, DELETE 5/h.
  • Admin deletes use a constant-time token comparison.
  • All input validated with Zod; D1 queries are parameterised through Drizzle.

Edge runtime

Everything runs on Cloudflare's edge, with its constraints.

  • API routes use the edge runtime: Web APIs only, no Node built-ins.
  • D1 and KV are bindings on the request context, not network clients.
  • The Next.js version is capped by the @cloudflare/next-on-pages adapter.
  • Tiles, geocoding and city search are called from the browser; the edge only reverse-geocodes as a fallback.

Stack

Built with AI agents

The same agentic workflows I design at work are used here: the repo is written to be read by coding agents as much as by people.

  • AGENTS.md is the single entry point for any coding agent: stack, structure and a pointer to the docs.
  • Conventions, architecture and workflow live in docs/ as rules an agent can follow, not tribal knowledge.
  • Work starts from a GitHub issue with explicit acceptance criteria that double as the agent's definition of done.
  • Each issue gets its own git worktree and branch, so agents work in isolation and land through a reviewed PR.
  • Lint, type-check, tests and the Cloudflare preview build gate every merge.

Deployed from commit 4bebc0f