Visitor Geolocation for Analytics — Without the Cookie Banner

Get country, region, city, ISP, and device context from the server — before any JS runs, before any cookie is set.

Privacy-first analytics is the new default. Plausible, Umami, Fathom, PostHog self-hosted — they all avoid third-party cookies. But they still need to answer one question your product manager asks every Monday: “Where are our users?” IP geolocation answers that at the server, with no tracking pixels and no banner.

The business problem

Traditional analytics used third-party cookies + GeoIP databases shipped with the client. GDPR, PECR, and ITP killed that pattern:

IP geolocation at the server returns country / region / city / ISP / timezone from the IP alone — no fingerprinting, no cookie, no consent banner needed (recital-free processing under GDPR Art. 6(1)(f) legitimate interest, because the IP is a transient network identifier, not stored with personal data).

Implementation

Server-side logging (append to every page-view event)

// Express / Fastify middleware
import { ipgeoLookup } from "./lib/ipgeo.js";

app.use(async (req, res, next) => {
  const ip = req.ip;
  const geo = await ipgeoLookup(ip);

  // Log the event; drop IP after lookup to stay consent-free
  analytics.track("pageview", {
    path: req.path,
    country: geo.country_code,
    region: geo.region,
    city: geo.city,
    asn: geo.asn,
    is_mobile_network: geo.connection_type === "mobile",
    timestamp: Date.now()
    // NOTE: no raw IP stored → no personal data under most GDPR interpretations
  });
  next();
});

Plausible / PostHog enrichment

If you self-host Plausible or PostHog, swap their bundled MaxMind DB for a lookup call:

// Plausible plugin (or any custom ingestion endpoint)
export async function enrichEvent(event) {
  const geo = await ipgeoLookup(event.ip);
  return {
    ...event,
    country_code: geo.country_code,
    region: geo.region,
    city: geo.city,
    isp: geo.org,
    is_eu: geo.is_eu,
    timezone: geo.timezone
  };
}

Batch enrichment (historic log backfill)

# Pipe a CSV of IPs into our batch endpoint (100 per call)
jq -c '{ips: .}' ips.json \
  | curl -X POST https://api.ipgeo.10b.app/v1/lookup/batch \
    -H "Authorization: Bearer $IPGEO_API_KEY" \
    -H "Content-Type: application/json" \
    -d @-

Useful for enriching archived server logs, S3 access logs, or Cloudflare logpush output.

Why IP Geo API for this use case

Pricing math

For most analytics deployments, you cache lookups for 1+ hours per IP, so the cost scales with unique IPs, not page-views.

Monthly unique IPs Tier Cost/mo
< 30 K Free € 0
< 1 M Starter € 29
< 10 M Business € 99

A blog with 300 K monthly unique visitors → € 0 (free tier, assuming aggressive caching). A high-traffic media site with 5 M uniques → € 99.

Honest trade-offs

Privacy note

IP geolocation without storing the raw IP past the lookup is widely considered pseudonymous processing under GDPR. Best practice:

  1. Look up the IP on request.
  2. Store only the aggregated dimensions (country, region, city).
  3. Drop the raw IP before the event reaches your warehouse.

This removes the “personal data” classification for the stored event and simplifies your privacy policy.

Related use cases

Get started

Free tier: 1 000 lookups / day → /pricing. Sign up at https://ipgeo.10b.app/pricing.


Get early access — 50% off for 12 months

First 100 signups lock in 50% off any paid plan for the first year. No credit card required — we’ll email you at launch.