Skip to Content

Next.js

Next.js has two different proxy mechanisms depending on which mode you need.

  • Simple mode: a one-liner in next.config.js. Done.
  • Full mode: requires a Route Handler (or Edge Function). A plain rewrites() config proxies server-side and drops the visitor IP, which breaks geolocation. The Route Handler reads the IP and re-sends it.

Snow’s Settings → Proxy tab generates the right code for each mode.


Add one rewrite to your Next.js config. That’s it.

// next.config.js module.exports = { async rewrites() { return [ { source: "/sa/sa.js", destination: "https://api.snowanalytics.app/sa.js", }, ]; }, };

Then update your embed to add data-api-url:

<script defer src="https://yourdomain.com/sa/sa.js" data-site="YOUR_SITE_ID" data-api-url="https://api.snowanalytics.app/api/collect" data-domain="yourdomain.com"></script>

The data-api-url attribute tells sa.js to post events directly to the Snow API. The browser sends them; your Next.js server is not in the path for events, so the real visitor IP reaches Snow untouched.

If you’re using the Next.js install guide, this replaces the plain src URL. The rewrite and data-api-url together are what make it first-party.


Full mode (script and events)

Full mode routes both the script and events through your Next.js app. Because events go through your server, you must forward the real visitor IP. Otherwise every visitor geolocates to your server’s city.

Why a Route Handler, not rewrites()? Next.js rewrites() proxy requests server-side and drop the original client IP. A Route Handler (or Edge middleware) runs in the request context where you can read X-Forwarded-For or request.ip and re-emit it in the headers you send to Snow.

Add the script rewrite

Keep this in next.config.js. rewrites() is fine for the script because no IP forwarding is needed there.

// next.config.js module.exports = { async rewrites() { return [ { source: "/sa/sa.js", destination: "https://api.snowanalytics.app/sa.js", }, ]; }, };

Enable Secure proxy

In Settings → Proxy, switch to Full mode and enable Secure proxy. Copy the secret. It’s shown once.

Create the Route Handler

Create this file at app/sa/api/collect/route.ts (App Router). It reads the visitor’s IP from the incoming request and forwards it with the secret.

// app/sa/api/collect/route.ts import { NextRequest } from "next/server"; const SNOW_API = "https://api.snowanalytics.app/api/collect"; const PROXY_SECRET = process.env.SNOW_PROXY_SECRET; if (!PROXY_SECRET) { throw new Error("SNOW_PROXY_SECRET env var is not set"); } export async function POST(request: NextRequest) { // Read the real visitor IP. Your reverse proxy (Nginx, Caddy, …) sets it in // x-forwarded-for or x-real-ip. Locally, fall back to a placeholder. const ip = request.headers.get("x-forwarded-for")?.split(",")[0].trim() || request.headers.get("x-real-ip") || "127.0.0.1"; const headers = new Headers(); headers.set("content-type", request.headers.get("content-type") || "text/plain"); headers.set("user-agent", request.headers.get("user-agent") || ""); headers.set("X-Snow-Client-IP", ip); headers.set("X-Forwarded-For", ip); headers.set("X-Snow-Proxy-Secret", PROXY_SECRET); const response = await fetch(SNOW_API, { method: "POST", headers, body: await request.text(), }); return new Response(response.body, { status: response.status, headers: { "content-type": response.headers.get("content-type") || "text/plain" }, }); }

Set the environment variable

Add to .env.local (and to your deployment environment):

SNOW_PROXY_SECRET=your_secret_from_settings

Update your embed

<script defer src="https://yourdomain.com/sa/sa.js" data-site="YOUR_SITE_ID" data-api-url="/sa/api/collect" data-domain="yourdomain.com"></script>

data-api-url is now a relative path. The browser posts events to your Next.js Route Handler, which forwards them to Snow with the correct headers.

Verify

Generate some traffic, then check Analytics → Locations. A correct setup shows a realistic country distribution, not everything from one city. See Verify it’s working.