| index a75e46179..2ad1175c2 100644 |
| --- a/docs/site/cli.md |
| +++ b/docs/site/cli.md |
| @@ -994,6 +994,47 @@ carlos analytics --app hello --range 7d |
| visitors summed. The same view is the Analytics tab on the app's dashboard, |
| and the switch is on the Settings tab too. |
| |
| +When the deployment has a GeoIP database, page views are also counted by |
| +country. A visitor the database could not place counts as `unknown`. The |
| +Analytics tab credits the database the counts came from. |
| + |
| +### Headers your app receives |
| + |
| +On a deployment with a GeoIP database, every request the edge passes to your |
| +app carries the visitor's rough location: |
| + |
| +| Header | Value | |
| +|---|---| |
| +| `X-Carlos-Geo-Country` | the country, as an ISO 3166-1 code such as `IE` | |
| +| `X-Carlos-Geo-Subdivision` | the state or province, as an ISO 3166-2 code such as `US-WA`. Only some databases have these | |
| +| `X-Carlos-Geo-Lat`, `X-Carlos-Geo-Lon` | a point, to two decimal places | |
| +| `X-Carlos-Geo-Source` | which database answered, such as `DBIP-City-Lite` | |
| + |
| +Any of them can be missing. They are all missing when the deployment has no |
| +database, when the database has no answer for the address, and on calls |
| +through the edge-local door, where the caller is a server rather than a |
| +visitor. Treat a missing header as "unknown", never as an error. |
| + |
| +The point is an area, not a position. These databases place an address to |
| +the right city at best, and often only to a region or a country. |
| + |
| +The edge removes any `X-Carlos-Geo-` header a visitor sends, so the values |
| +your app sees are always the platform's. The visitor's address is still in |
| +`X-Forwarded-For` if you need it. |
| + |
| +If you cache a page that differs by country, send `Vary: |
| +X-Carlos-Geo-Country`. The edge keeps at most eight variants of one URL, so a |
| +page with visitors from many countries will be served from cache less often. |
| +Don't vary on the coordinates: almost every visitor would get their own |
| +variant. |
| + |
| +**Credit the database.** If your app shows visitors anything worked out from |
| +these headers, it must credit the database `X-Carlos-Geo-Source` names: |
| + |
| +- `DBIP-…`: "IP Geolocation by DB-IP", linked to https://db-ip.com |
| +- `GeoLite2-…`: "This product includes GeoLite2 data created by MaxMind, |
| + available from https://www.maxmind.com" |
| + |
| ### carlos store |
| |
| Object storage for an app. An account owner applies once, and a deployment |
| @@ -1764,6 +1805,50 @@ Deployment operators only (`CARLOS_STORE_OPERATORS`). Enabling scans nothing by |
| itself — a member still requests scanning for their app and an operator still |
| grants it per store. |
| |
| +#### Platform GeoIP |
| + |
| +```sh |
| +carlos system geoip publish dbip-city-lite-2026-10.mmdb |
| +carlos system geoip status |
| +carlos system geoip off |
| +``` |
| + |
| +`publish` sends a MaxMind-format database, DB-IP Lite or GeoLite2, to the |
| +console. The console checks it, stores it in the deployment bucket, and every |
| +edge picks it up within 11 minutes with no restart. From then on, every |
| +request to every app carries the headers described under |
| +[Headers your app receives](#headers-your-app-receives), and analytics count |
| +page views by country. `publish` refuses a file that isn't a valid database, |
| +has no country data, or is a GeoLite2 database already past its 30 days. It |
| +also refuses one with under half the current database's nodes, which usually |
| +means a truncated file. `-force` overrides that last check only. |
| + |
| +`status` shows the published database and every box: whether it runs a build |
| +that removes forged geo headers, and whether it has the current database. |
| +Its last line says `ready` only when every box does. Publish a database only |
| +after the whole fleet is ready, and tell apps about the headers only after |
| +that. Until every box has been updated, an older box passes a visitor's |
| +forged headers straight through. |
| + |
| +A box that has been retired but still has an edges record keeps `status` from |
| +saying `ready`. Deleting its record (`control/edges/<label>.json`) retires it. |
| + |
| +`off` removes the database. Every edge stops sending the headers within 11 |
| +minutes. |
| + |
| +**The licence is yours to keep.** DB-IP Lite is licensed under CC BY 4.0 |
| +(https://db-ip.com/db/lite.php), so pages that show its data must credit it. |
| +The Analytics tab does this for you, and apps are told to do the same. |
| +GeoLite2 comes under MaxMind's GeoLite EULA |
| +(https://www.maxmind.com/en/geolite/eula), which also requires replacing the |
| +database within 30 days of a newer release. The platform enforces that by |
| +dropping a GeoLite2 database 30 days after it was built. Before you publish |
| +GeoLite2, apply the deployment-buckets module version that expires deleted |
| +GeoIP files after a day; the bucket is versioned, and otherwise old copies |
| +would linger. |
| + |
| +Ops account owners and `CARLOS_SYSTEM_OPERATORS`. |
| + |
| ## On the host |
| |
| These are what systemd starts. You will rarely type them, but knowing what |