Why the IP can’t be read client-side
The browser has no native API to read its own public IP address. window.location gives you the page URL. WebRTC can expose local network addresses, but not the public IP your traffic exits from. The only reliable way to know your public IP is to ask a server — the server sees the IP on the incoming connection and reports it back.
This is why ShowMyIP uses SvelteKit’s +page.server.ts instead of a client-side fetch. The IP detection must happen on the server before the page is rendered. The result is passed as page data and used directly in the template.
The proxy header chain
The obvious approach — read the IP from the TCP connection — doesn’t work when there’s a proxy in the path. Vercel routes requests through its own internal load balancer, so the TCP connection to the SvelteKit function comes from a Vercel infrastructure IP, not from your machine.
Vercel solves this the standard way: it injects the real client IP into request headers before passing the request downstream. The code reads headers in priority order, taking the first value it finds:
x-real-ip is checked first because Vercel always sets it to a single clean IP — no parsing needed. x-forwarded-for is the fallback: it can contain a comma-separated chain when the request passed through multiple proxies, so the code takes the first (leftmost) value and trims whitespace.
MaxMind GeoLite2
Once the IP is known, the geolocation lookup runs against two local binary databases:
- GeoLite2-City.mmdb (63 MB) — maps IP ranges to country, city, postal code, coordinates, and timezone
- GeoLite2-ASN.mmdb (12 MB) — maps IP ranges to ISP name, organization, and AS number
Lookups are in-process binary searches on a memory-mapped file. There is no network call, no connection pool, no external dependency at query time — a typical lookup takes around 1 ms.
The singleton reader
MaxMind’s reader objects are expensive to initialize — they open and memory-map a 12–63 MB file. Creating one per request would waste time and memory. Instead, the module keeps module-level singletons:
let cityReader: Reader<CityResponse> | null = null;
let asnReader: Reader<AsnResponse> | null = null;
let maxmindReady: boolean | null = null; The first request to a cold instance pays the initialization cost (roughly 100 ms to open both files). Every subsequent request in the same warm instance reuses the open readers at no additional cost. On serverless, “same instance” means the same execution context — Vercel keeps function instances alive between requests for a while before discarding them. Under steady traffic, cold starts are infrequent.
The maxmindReady flag avoids re-attempting the load on every request if initialization failed once — it fails fast on the second call instead of retrying a broken path.
Hostname resolution
The hostname field comes from a DNS reverse lookup — a PTR query against the IP address. Most consumer IPs don’t have a PTR record, so the result is often empty. It runs with a 1.5-second timeout to avoid slowing down the response for IPs that don’t answer:
await Promise.race([
dns.reverse(ip),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 1500))
]); The public API
GET /api/ip returns the same IpInfo object as the page. Two headers make it usable from anywhere:
Access-Control-Allow-Origin: *— any browser script can call it without CORS issuesCache-Control: no-store— every response reflects the current request’s IP, never a cached one