Browsers and servers don’t magically know the user’s exact location.
You really only have two serious options:
This is the one everyone thinks of:
- Runs in the browser
- Uses the device’s GPS / Wi-Fi / network
- Shows a permission popup to the user
Example output:
{ "lat": 28.6139, "lng": 77.2090 }You can then hit your backend with that and query for nearby stores.
- Very accurate (street or building level)
- Good for “nearest store” / “delivery in 10 mins” style apps
- User might click “Block” and then you’re done
- Only works on the client (no use for SSR/SEO on first load)
- Not ideal to trigger on page load, or users bounce
This is the server-side friendly version.
- Uses the user’s IP to guess location
- Works before the page renders (SSR / Edge)
- Accuracy is more like city or region, not street
You usually plug in a provider:
- MaxMind
- IP2Location
- IPinfo
- Cloudflare Geo headers
- Vercel Edge Geo (if you deploy on Vercel)
- No permission popup
- Works on first page load
- Great for SEO and initial experience
- Good enough to say “show stores in Delhi” or “Mumbai”
- Not precise enough for “nearest store is 450m away”
- VPN / proxies can mess it up
- Sometimes guessed city is wrong
The sane, production-style flow is:
- Figure out country / region / city from IP
- Based on that, pick local stores or inventory
- Render page with those recommendations already there
So users see something relevant instantly.
Then:
- If they want super accurate “near me” results
- Or when they tap a button like “Use current location”
This is close to how Zomato, Swiggy, Blinkit, Instamart, etc. do it:
- Coarse location from IP for defaults
- Precise location only when user is clearly interested
On Vercel, you get geo injected into the request at the Edge.
src/middleware.ts:
import { NextResponse } from "next/server";
export function middleware(req: Request) {
// Available on Vercel Edge
const geo = (req as any).geo;
return NextResponse.next({
headers: {
"x-country": geo?.country || "",
"x-region": geo?.region || "",
"x-city": geo?.city || "",
"x-latitude": geo?.latitude || "",
"x-longitude": geo?.longitude || ""
}
});
}This middleware:
- Runs before your route
- Reads geo info
- Attaches it as headers for your app to use
Example idea (pseudo-ish):
export default async function Page({ req }: { req: Request }) {
const city = req.headers.get("x-city");
const lat = req.headers.get("x-latitude");
const lng = req.headers.get("x-longitude");
const stores = await getNearbyStores({ lat, lng });
return <StoreList stores={stores} />;
}Flow:
- Middleware adds geo headers
- Server component / API handler reads them
- You fetch stores close to that location
- Page loads with localized data from the first render
(Exact access to req depends on whether you’re using App Router / Pages Router / Route Handlers, but concept is the same.)
Then you just do IP lookup yourself on the server.
Example:
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
// Call IP provider
const res = await fetch(`https://ipinfo.io/${ip}?token=YOUR_TOKEN`);
const data = await res.json();
// Example: "28.6139,77.2090"
const [lat, lng] = data.loc.split(",");Use lat and lng from here to do:
- “Stores near user city”
- “Default city selection”
- “Top products around you”
On the client, when user clicks a button like “Use my location”:
navigator.geolocation.getCurrentPosition(pos => {
const { latitude, longitude } = pos.coords;
fetch("/api/geo", {
method: "POST",
body: JSON.stringify({ latitude, longitude })
});
});- Do not ask for GPS the moment the page loads
- Ask only after intent: user clicks “Use current location”
- Explain why: “We’ll show you the closest store and delivery time”
Users are way more likely to accept it when it’s contextual.
- Detect location roughly (city/region)
- Show reasonable defaults for local catalog
- Good for SEO and first impression
-
Show a button: “Use precise location”
-
When user taps:
- Request geolocation
- Hit backend with coordinates
- Return truly nearest stores / slots / offers
This is the balance between UX and not being creepy/annoying.
You cannot play dumb here.
-
Don’t store precise coordinates unless you actually need them
-
If you store, say why and for how long
-
Be careful logging things like:
- Access logs with full GPS
- Debug logs with user coordinates
You are in the territory of GDPR, CCPA, etc. so treat location data as sensitive.
Here’s how people mess this up:
- Blocking the UI waiting for GPS on first load
- Triggering location permission immediately when page opens
- Sending raw
lat/lngin query params everywhere - Dumping thousands of stores to client and filtering there
- Building the whole location logic only on client (bad SEO + bad initial UX)
| Purpose | Option |
|---|---|
| Server IP lookup | Vercel Geo / IPinfo / MaxMind |
| Map / UI | Google Maps / Mapbox |
| Store distance | Haversine formula / PostGIS |
You don’t have to go overboard on day 1, but this is the direction.
If you use Postgres + PostGIS:
SELECT id, name,
ST_Distance(
geography(ST_Point(lng, lat)),
geography(ST_Point($lng, $lat))
) AS distance
FROM stores
ORDER BY distance
LIMIT 10;This gives you nearest stores by distance from given coordinates.
If you want solid UX and not annoy users:
-
IP-based geo on the server
- Detect city/region automatically
- Render page with local products
-
Button to enable precise GPS
- “Show nearest store”
- Use Geolocation API only when user asks
-
Cache aggressively
- Same city → same recommendations for many users
- Use CDN and edge caching
This is more or less what serious consumer apps do.
Sweet spot:
- Deploy on Vercel
- Use Vercel Edge Geo
- Add middleware to inject headers
- Use server components / route handlers
- Use DB with geospatial support (PostGIS, Mongo with 2dsphere, etc.)