TL;DR: Geo enrichment is live for new traffic from 2026-05-06. Every public IP now resolves to a country code via a local geoip-lite database lookup. No IPs leave our infrastructure — the same privacy model used by Plausible and Fathom. Historical rows are not backfilled (we don’t persist raw IPs), so pre-2026-05-06 country data stays as Unknown. New rows from now on get a country.
Admaxxer’s pixel ingest used to read the visitor’s country from a single header: cf-ipcountry. That header is set automatically by Cloudflare’s edge network when traffic flows through Cloudflare on its way to our ingest endpoint — no extra work required, two-letter ISO country code arrives in the request.
But Admaxxer’s ingest endpoint runs on a bare-metal Hetzner box in Hillsboro, Oregon, with no Cloudflare proxy in front. For customer traffic that doesn’t pass through Cloudflare’s edge on its way to /api/event, the cf-ipcountry header was never set. The pixel row still wrote successfully — everything else worked — but country came in as the empty string. Downstream, the Geo card on /dashboard rendered every row as “Unknown,” making the card effectively useless for affected workspaces.
This isn’t a customer-side issue — it’s a server-side gap on our end. Whether the customer is on Cloudflare, Vercel, Netlify, AWS CloudFront, or no CDN at all, the pixel call ends up at our ingest endpoint and we shouldn’t have been depending on a single CDN-set header. Now we don’t.
Added server/lib/geo/resolveCountry.ts — a deterministic resolution chain that runs on every /api/event request. Tries cf-ipcountry first (still preferred when the customer’s site is behind Cloudflare — the edge is more accurate than IP databases for VPN/proxy detection), falls through to other CDN-set country headers, then falls back to a local IP-to-country lookup using the geoip-lite npm package — the same library used by Plausible, Fathom, and most privacy-first analytics tools. As long as the IP is public and known to MaxMind’s GeoLite2 database, every visitor now gets a country.
On every ingest request, the server walks this chain in order. The first non-empty result wins:
x-vercel-ip-country (Vercel edge), x-real-ip-country, etc. Picked up automatically when present, in case some customer traffic flows through a non-Cloudflare CDN that still tags country at the edge.geoip-lite. Local lookup, no third-party API call. Returns the country code from MaxMind’s GeoLite2 database.x-forwarded-for chain (typically the original client IP before any proxy hops) and runs geoip-lite on it.'' and the row writes as Unknown. Should be under 5% of typical DTC traffic.If new traffic is still resolving as Unknown after the fix date, walk these checks in order. Most workspaces hit one of the first three.
cf-connecting-ip / x-forwarded-for, so the pixel sees the real client IP. If you’re behind a custom proxy that rewrites all incoming IPs to its own, every row will resolve to the proxy’s home country (or Unknown). Confirm via your CDN dashboard that x-forwarded-for is preserved end-to-end.geoip-lite ships. If your audience skews privacy-conscious (a privacy-tooling brand, a B2B SaaS sold to security buyers, etc.), expect a higher Unknown share than a typical DTC store. Anything under ~5% Unknown is healthy.visitor_id. We’ll trace the request lifecycle from pixel hit to Tinybird row.geoip-lite ships a binary database file (the MaxMind GeoLite2 Country DB) directly inside the npm package. The lookup runs locally inside the request handler — no outbound HTTP call, no third-party API, no IP-to-country service. The visitor IP is held in memory only for the duration of the single request, then discarded. Only the resolved country code persists to Tinybird. This is the same pattern Plausible and Fathom document in their privacy policies.
Admaxxer's pixel ingest reads country from the cf-ipcountry HTTP header — set automatically by Cloudflare's edge. That works perfectly when the customer's site is behind Cloudflare, which is most of the internet. But Admaxxer's own ingest endpoint (api.admaxxer.com via /api/event) is hosted on bare Hetzner, no Cloudflare proxy in front. For customer traffic that lands directly at our endpoint without passing through Cloudflare's edge, the cf-ipcountry header was never set and the pixel row got country=''. Result: rows wrote successfully, but the Geo card on /dashboard rendered Unknown for the country column. The fix is a server-side IP-to-country lookup using geoip-lite — the same library Plausible, Fathom, and most privacy-first analytics tools use.
On every /api/event request, the server now resolves country via this chain: (1) cf-ipcountry header (still preferred when present — Cloudflare's edge is more accurate than IP databases for VPN/proxy detection); (2) x-vercel-ip-country / x-real-ip-country / similar CDN-set headers; (3) request.ip (Express-resolved client IP) → geoip-lite lookup; (4) x-forwarded-for first-hop → geoip-lite lookup as a final escape. If every step returns nothing, country defaults to '' and the row writes Unknown. The chain is implemented in server/lib/geo/resolveCountry.ts.
No. Admaxxer never persists raw visitor IPs after the ingest request — we resolve country at ingest time and write only the country code. That's the right privacy model (no IP is ever at-rest in our DB) but it means we can't replay old rows through the new chain because we don't have the IP anymore. Rows that landed pre-2026-05-06 will continue to read Unknown for country forever. Going forward, every new row gets a country code as long as the IP is public.
No. geoip-lite ships a binary database file (the MaxMind GeoLite2 Country DB) directly with the npm package. The lookup runs locally inside the Express request handler — no outbound HTTP call, no third-party API, no IP ever leaves Admaxxer's infrastructure for geo classification. The IP is only held in memory for the duration of the single request, then discarded. Only the resolved country code (e.g., 'US', 'CA', 'GB') persists to the Tinybird row. This is the same pattern Plausible documents in their privacy policy.
On typical DTC traffic, expect 95-98% resolved (real country code) and 2-5% Unknown. The Unknown bucket is some combination of: (a) privacy/VPN/Tor exit IPs not in MaxMind's DB, (b) datacenter IPs that aren't user-facing, (c) legacy bot traffic, (d) very-recently-allocated IP ranges that haven't propagated to MaxMind yet. If you're seeing >10% Unknown sustained, check the still-empty checklist below — there's likely a CDN-IP-stripping issue rather than a geoip-lite limitation.
Yes — but only behaviorally, not visually. The card still renders the same chart and table; the country column just has fewer empty rows now. If you had a workspace where the Geo card felt useless because every row read Unknown, that workspace now shows real countries on every new pageview.
Dashboard card reference · Missing attribution data · Attribution discrepancies · Missing orders · Sales mismatch · How data works