carlos / platform Public

geoip: paid MaxMind feeds (geoip2-city, geoip2-country); spec rev 13 records Paul's rulings R7-R9 (D1, D2 accepted; free DB-IP by default, paid MaxMind by choice)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev a9d9746407350dc42ca90cb2272598c3f12d9235 parent b0b97e9
4 files changed, +56 −7
  • docs/superpowers/specs/2026-10-06-platform-geoip-design.md +29 −5
  • internal/console/geoipfeed.go +1 −1
  • internal/geoip/archive_test.go +20 −1
  • internal/geoip/feed.go +6 −0

CI cancelled — superseded by a newer push — run details

diff --git a/docs/superpowers/specs/2026-10-06-platform-geoip-design.md b/docs/superpowers/specs/2026-10-06-platform-geoip-design.md
index 45501503a..1ab5f75fc 100644
--- a/docs/superpowers/specs/2026-10-06-platform-geoip-design.md
+++ b/docs/superpowers/specs/2026-10-06-platform-geoip-design.md
@@ -1,7 +1,7 @@
# Platform GeoIP for apps: headers, a mirrored database, a lookup endpoint
**Date:** 2026-10-06
-**Revision:** 12
+**Revision:** 13
**Discussion:** amadan carlos/platform #241 (quoted in Appendix A)
**Branch:** `geoip-for-apps`
@@ -84,7 +84,12 @@
clock), so expiry never clears an alert while the bucket is still
unmarked.
- **Astra's round 12 (`CODEX-FINDINGS-GEOIP-R12.md`) accepted rev 12 with no
- findings.** The spec awaits Paul's sign-off on D1 and D2.
+ findings.**
+- **Rev 13 records Paul's rulings of 2026-10-07:**
+ - he accepts D1 and D2 (now R7 and R8);
+ - the free DB-IP Lite is the default, and operators (the flagship
+ included) may upgrade to paid MaxMind through two more feeds,
+ `geoip2-city` and `geoip2-country` (R9).
## 0. Rulings and decisions
@@ -158,6 +163,25 @@ reverse.
caches always matches what the app saw, whatever the transport, on
reconnect and on fleet fallback.
+- **R7, R8 (Paul, 2026-10-07):** D1 and D2 above are accepted as they
+ stand.
+- **R9 (Paul, 2026-10-07): free by default, paid by choice.**
+ - The documented default feed is `dbip-city-lite`. DB-IP Lite is free,
+ CC BY 4.0, and allows commercial use with the attribution the
+ platform already shows.
+ - Operators, the flagship included, may upgrade to MaxMind's paid
+ databases with `geoip2-city` or `geoip2-country`. These use the same
+ download API and credentials as GeoLite2.
+ - Paid editions are not GeoLite2, so D1's expiry does not apply, and
+ the platform shows no attribution for them: the operator's MaxMind
+ licence governs.
+ - The feed stays off until `CONSOLE_GEOIP_FEED` is set, so no
+ deployment starts downloading, or takes on a licence, without its
+ operator choosing to.
+ - GeoLite2 remains supported, but the operator docs note that MaxMind
+ calls it not recommended for commercial use and that its EULA limits
+ redistribution, which serving answers to customer apps may engage.
+
## 1. Why, and what this is not
The nearest-region work gave the console a GeoIP reader (`internal/geo`,
@@ -951,9 +975,9 @@ process per deployment, so there is only ever one fetcher.
| Variable | Meaning |
|---|---|
-| `CONSOLE_GEOIP_FEED` | `dbip-city-lite`, `dbip-country-lite`, `geolite2-city`, `geolite2-country`; unset means no feed |
-| `CONSOLE_GEOIP_MAXMIND_ACCOUNT` | MaxMind account ID, for `geolite2-*` |
-| `CONSOLE_GEOIP_MAXMIND_KEY` | MaxMind license key, for `geolite2-*` |
+| `CONSOLE_GEOIP_FEED` | `dbip-city-lite` (the documented default, R9), `dbip-country-lite`, `geolite2-city`, `geolite2-country`, and the paid `geoip2-city` and `geoip2-country`; unset means no feed |
+| `CONSOLE_GEOIP_MAXMIND_ACCOUNT` | MaxMind account ID, for `geolite2-*` and `geoip2-*` |
+| `CONSOLE_GEOIP_MAXMIND_KEY` | MaxMind license key, for `geolite2-*` and `geoip2-*` |
- An unknown feed name, or a GeoLite2 feed with no credentials, is logged
once at startup. The feed stays off and the console still starts.
diff --git a/internal/console/geoipfeed.go b/internal/console/geoipfeed.go
index 46e7eb493..e43b4c53f 100644
--- a/internal/console/geoipfeed.go
+++ b/internal/console/geoipfeed.go
@@ -53,7 +53,7 @@ func newGeoipFeeder(cfg Config, pub *geoipPublisher) (*geoipFeeder, bool) {
}
spec, ok := geoip.ParseFeed(name)
if !ok {
- log.Printf("console: geoip feed misconfigured: CONSOLE_GEOIP_FEED %q is not one of dbip-city-lite, dbip-country-lite, geolite2-city, geolite2-country; the feed is off", name)
+ log.Printf("console: geoip feed misconfigured: CONSOLE_GEOIP_FEED %q is not one of dbip-city-lite, dbip-country-lite, geolite2-city, geolite2-country, geoip2-city, geoip2-country; the feed is off", name)
return nil, false
}
if spec.Provider == "maxmind" && (cfg.GeoipMaxMindAccount == "" || cfg.GeoipMaxMindKey == "") {
diff --git a/internal/geoip/archive_test.go b/internal/geoip/archive_test.go
index 030a3903d..768c8bcf9 100644
--- a/internal/geoip/archive_test.go
+++ b/internal/geoip/archive_test.go
@@ -87,7 +87,7 @@ func TestFeedSpecsAndURLs(t *testing.T) {
if got := MaxMindURL("", g, "tar.gz.sha256"); got != "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz.sha256" {
t.Fatalf("maxmind url = %s", got)
}
- if _, ok := ParseFeed("geoip2-city"); ok {
+ if _, ok := ParseFeed("ipinfo-lite"); ok {
t.Fatal("unknown feed accepted")
}
}
@@ -109,3 +109,22 @@ func TestFeedStatusFailing(t *testing.T) {
t.Fatalf("parse = %+v %v", got, err)
}
}
+
+// The paid MaxMind upgrade (Paul, 2026-10-07): same API, no D1 expiry.
+func TestPaidMaxMindFeeds(t *testing.T) {
+ for name, typ := range map[string]string{"geoip2-city": "GeoIP2-City", "geoip2-country": "GeoIP2-Country"} {
+ s, ok := ParseFeed(name)
+ if !ok || s.Type != typ || s.Provider != "maxmind" {
+ t.Fatalf("%s = %+v %v", name, s, ok)
+ }
+ if got := MaxMindURL("", s, "tar.gz"); got != "https://download.maxmind.com/geoip/databases/"+typ+"/download?suffix=tar.gz" {
+ t.Fatalf("%s url = %s", name, got)
+ }
+ if Expired(typ, time.Unix(0, 0), time.Now()) {
+ t.Fatalf("%s must never expire under D1", typ)
+ }
+ if _, ok := AttributionFor(typ); ok {
+ t.Fatalf("%s has no platform-known attribution", typ)
+ }
+ }
+}
diff --git a/internal/geoip/feed.go b/internal/geoip/feed.go
index 9c176a006..f6d385581 100644
--- a/internal/geoip/feed.go
+++ b/internal/geoip/feed.go
@@ -16,6 +16,12 @@ var feeds = map[string]FeedSpec{
"dbip-country-lite": {"dbip-country-lite", "DBIP-Country-Lite", "dbip"},
"geolite2-city": {"geolite2-city", "GeoLite2-City", "maxmind"},
"geolite2-country": {"geolite2-country", "GeoLite2-Country", "maxmind"},
+ // The paid upgrade (Paul, 2026-10-07): MaxMind's commercial databases,
+ // the same download API and credentials. Not GeoLite2, so no D1 expiry,
+ // and no attribution the platform knows of: the operator's MaxMind
+ // licence governs.
+ "geoip2-city": {"geoip2-city", "GeoIP2-City", "maxmind"},
+ "geoip2-country": {"geoip2-country", "GeoIP2-Country", "maxmind"},
}
// ParseFeed resolves CONSOLE_GEOIP_FEED.