Expose the platform's GeoIP lookup to apps (headers and/or lookup endpoint) Feature Requestclosed
Claimed by claude~paul@keymail.dev2 weeks ago
claude agent, operated by paul@keymail.devoperated by paul@keymail.devsession feb76a76 · claude-code/2.1.291 · DOCS-T1
Why
The nearest-region-default spec (docs/superpowers/specs/2026-10-01-nearest-region-default-design.md, §6, on branch edge-replica-placements) gives the console an optional GeoIP reader: CONSOLE_GEOIP_DB names an operator-supplied .mmdb (DB-IP Lite or MaxMind GeoLite2), read through github.com/oschwald/maxminddb-golang, mapped and opened once per process. Today it serves one caller: guessing a new app's region from the requester's IP.
Once the platform carries that database, every app on it is one lookup away from "roughly where is this visitor", and none of them should have to ship their own .mmdb, handle attribution, or track database updates. The platform should expose the lookup to apps.
What
A platform geolocation facility that any app can use, backed by the same database and reader the console already has. Two shapes are worth weighing; the agent picking this up should choose one (or both) and say why in the spec:
Router-injected request headers. The edge/router does the lookup on the client IP and sets headers on the proxied request, e.g. X-Carlos-Geo-Country, X-Carlos-Geo-Region, X-Carlos-Geo-Lat / -Lon, X-Carlos-Geo-Source. Zero integration work for apps; works in every language; covers the common case (country/currency/locale defaults, "nearest region" hints). Cost: a lookup on every request unless cached per connection or per IP.
A lookup endpoint. A platform-internal HTTP endpoint apps can call with an arbitrary IP (GET /geo?ip=… on the sidecar/hop path, authenticated the way other platform-internal calls are). Needed for anything that isn't "the current request": batch jobs, IPs from logs, webhooks carrying a client IP.
Headers alone probably cover 90% of app needs; the endpoint covers the rest. Shipping headers first and the endpoint as a follow-on is a reasonable sequence.
Constraints to carry over from the spec
Unset CONSOLE_GEOIP_DB means the feature is off: headers absent / endpoint returns "unavailable", never a guess. Unopenable database: logged once, feature off, platform still starts.
Never look up loopback, private, link-local, CGNAT or unspecified addresses.
Lookup errors are a miss, logged at most once a minute.
Database replacement is an atomic rename plus restart; whatever process owns the lookup needs the same lifecycle.
Attribution: DB-IP Lite (CC BY 4.0) requires "IP Geolocation by DB-IP" with a link on pages that show the data. If apps surface the data, the platform docs must tell them about that obligation, and the platform's own pages that use it must carry it. GeoLite2 has its own EULA terms (attribution, keeping the data updated). The operator owns compliance; the docs should say so plainly.
The reader lives in (or is being added to) the console. If the router or a sidecar needs it, the reader should move to a shared internal package rather than being duplicated.
Open questions
Where does the lookup physically run: router, console, sidecar? Which one actually sees the client IP, and which one can memory-map the database? Only one place should open the file.
Header naming and whether to include coordinates or just country/subdivision. Coordinates make "nearest X" trivial for apps but are more than most need.
Should apps be able to opt out (an app config flag) to avoid the per-request cost when they don't care?
Does carlos CLI need anything (e.g. carlos geo <ip> for debugging what the platform would return)?
Related
Nearest-region default spec, §6 (IP lookup), §14 (region table). Build on its reader; do not add a second one.
claude agent, operated by paul@keymail.devoperated by paul@keymail.devsession fda9d0f8 · claude-code/2.1.291 · DOCS-T1
Picked up on branch geoip-for-apps. Spec rev 1: docs/superpowers/specs/2026-10-06-platform-geoip-design.md (5e1129be).
Paul's rulings, from the brainstorm:
R1: the .mmdb is published to the bucket once. Every edge mirrors it and hot-swaps its reader, with no restart.
R2: the headers are Country, Subdivision (ISO; GeoLite2 only), Lat/Lon (2 dp) and Source.
R3: tunnel and fleet boxes trust the hub's headers through a tunnel-only context mark.
R4: there is no per-app switch.
R5: one spec, three plans: (1) headers, mirror, publish and analytics countries; (2) the auto-updating feed; (3) the /geo endpoint and carlos geo.
R6: the console auto-fetches DB-IP Lite or GeoLite2.
The lookup runs on the edge (carlos agent, on every box), off the existing internal/geo reader. Astra review round 1 is running.
claude agent, operated by paul@keymail.devoperated by paul@keymail.devsession fda9d0f8 · claude-code/2.1.291 · DOCS-T1
Built overnight on geoip-for-apps. Nothing is merged or rolled.
Spec:docs/superpowers/specs/2026-10-06-platform-geoip-design.md rev 12, accepted by Astra in round 12 after 11 rounds. All findings were accepted, and the dispositions are in spec §18.
Plans:
Plan 1: the bucket mirror with hot swap, the headers, analytics countries, carlos system geoip publish|status|off.
Plan 2: the auto-update feed (DB-IP or MaxMind) and the geoip box alert.
Plan 3: the console follows the mirrored generation; GET /api/cli/apps/{a}/{app}/geo; carlos geo.
Code review: 13 Astra rounds. The last found nothing new. Fixed along the way:
forged-header bypasses, including Connection nomination and underscore aliases;
untrusted .mmdb files that hang, panic or exhaust the parser, now parsed only in a killable, memory-capped child;
crash-safety of the local generations.
Waiting on Paul:
Copy review: 89 UI/CLI strings and 22 doc paragraphs. The copy was written into files before review, so edits will be applied verbatim.
D1: GeoLite2 expires 30 days after build (EULA).
D2: the tunnel mark applies regardless of TLS. This reverses an earlier answer, because a plaintext tunnel's on-path attacker already controls every request.
Before roll: apply the deployment-buckets lifecycle rule, roll every box, run carlos system geoip status until ready, then publish.
Outstanding: GeoLite2 City has not been measured (no MaxMind account). The box-retire verb is filed as #249.
claude agent, operated by paul@keymail.devdriven by paul@keymail.devsession fda9d0f8 · claude-code/2.1.291 · DOCS-T1
Copy review: Paul approved all 89 UI/CLI strings and 22 doc paragraphs on 2026-10-07 with no edits; the code already carries them verbatim. Remaining before merge: D1/D2 sign-off.
claude agent, operated by paul@keymail.devdriven by paul@keymail.devsession fda9d0f8 · claude-code/2.1.291 · DOCS-T1
Merged to main as 6bdaba3 (squash, CI green) and mirrored to GitHub, 2026-10-07. Not rolled: part of today's big roll. Roll steps: (1) lifecycle rule (bump the carloku-infrastructure module ref, targeted apply; needed before any GeoLite2); (2) roll boxes and console; (3) carlos system geoip status until ready; (4) set CONSOLEGEOIPFEED=dbip-city-lite on the console, or carlos system geoip publish; (5) status until every box has the sha, then spot-check the headers. Leaving this open until it is live.
claude agent, operated by paul@keymail.devdriven by paul@keymail.devsession fda9d0f8 · claude-code/2.1.291 · DOCS-T1
Live on the flagship as of 2026-10-07.
Boxes: v0.25.1 (all four). Console: 289043db (the v0.25.1 tag), rolled with roll-console.sh flagship.
Feed: CONSOLE_GEOIP_FEED=dbip-city-lite, set in /etc/systemd/system/console.service.d/geoip.conf on the console box (i-092c0c1eea75723cb). First run published 9e250f02 (DBIP-City-Lite, built 2026-10-01); it checks daily from here.
carlos system geoip status: ready, all four boxes ok on 9e250f02.
carlos geo spot-check: an Irish address reads IE, 8.8.8.8 US, 1.1.1.1 AU, each with the DB-IP credit.
Apps now receive X-Carlos-Geo-Country, -Subdivision, -Lat, -Lon and -Source on every request through the edge. Pages that show the data must credit "IP Geolocation by DB-IP".
Not done: the deployment-buckets lifecycle rule (only needed before switching to GeoLite2 or paid MaxMind), and the box-retire verb in #249.