The country was in the header all along.
Two of the Next.js marketing sites I work on needed the visitor's country, to pick a phone dial code on lead forms and to show local defaults. Both got it by calling a paid IP-geolocation API from the browser, with the API key in a NEXT_PUBLIC_ variable. That meant the key was in the client bundle, and every page view spent quota.
Both sites sit behind Cloudflare, and Cloudflare adds a CF-IPCountry header to every request it forwards. The answer was already arriving for free, on the server.
The fallback chain
I moved detection server-side, into one small module with an explicit order:
CF-IPCountry, when present and valid. Cloudflare'sXX(unknown) andT1(Tor) values count as missing.- The paid API, server-side, only while its key is still configured. This was a temporary safety net for the rollout.
- None. The locale's defaults apply.
The forms now fetch a same-origin /api/geo route instead of the third-party API. The route returns the same field names the old API used, so the components barely changed. It also returns a source field saying which step answered.
A bug that had always been there
The middleware also set a country cookie. It read request.geo, or a Vercel-specific header. On our Docker hosting, neither exists, so the cookie had always said unknown, and the hook that read it silently fell back every time. Reading CF-IPCountry first made that code path work for the first time.
Rolling it out safely
The source field made the rollout checkable from any browser: open /api/geo on each domain and look for source: "cloudflare". Only once every domain reported that was it safe to delete the key and cancel the subscription.
Before shipping, I tested the chain against a local server:
- the header is used first, with no outbound call
- the real API fallback works (8.8.8.8 resolves to US, +1)
- with no key, the result is empty instead of an error
- page responses set the country cookie from the header
When I added the sites' first test suites later, the geo module was one of the first things covered, including a test that a Cloudflare hit never calls the paid API.
The result
- No API key in the browser. The environment variable lost its
NEXT_PUBLIC_prefix and became server-only. - A subscription that can be cancelled. The paid API is only a fallback during rollout.
- A cookie that finally works on the actual hosting.
The general lesson: before adding a service, check what your edge already tells you. CDNs forward a lot of useful context, like country, protocol, and client IP. Reading a header is cheaper, faster, and safer than calling someone else's API from the browser.

Henry Iddirisu
AI product engineer · Accra, Ghana · Remote