Serve the script from your own domain
Proxy the script and its requests through your own domain, with the proxy secret that forwards the visitor's real IP address safely.
Some ad blockers block analytics scripts, and the requests they send, when they come from another domain. You can
route them through your own site instead: your server forwards a path of your site, such as /_pf/, to PilotFox, and
the page loads the script from that path.
The script sends everything to the directory it was loaded from, so one path covers it all:
| The page requests | Your server forwards it to |
|---|---|
/_pf/s.js | https://pilotfox.app/api/s.js (the script) |
/_pf/c/SITE_ID | https://pilotfox.app/api/c/SITE_ID (the site's settings) |
/_pf/e, /_pf/i | https://pilotfox.app/api/e, /api/i (events, identify) |
/_pf/r.js, /_pf/rec/SITE_ID | https://pilotfox.app/api/r.js, /api/rec/SITE_ID (session replay, when on) |
The proxy secret
PilotFox counts visitors by IP address. Through a proxy, every
request comes from your server, so every visitor would share its address: one visitor per user agent, your server's
location, and your IP exclusions would never match. So your server passes each visitor's address in the
X-PilotFox-IP header, and the site's proxy secret in X-PilotFox-Proxy-Secret proves that the header comes from
your server.
- Without a valid secret,
X-PilotFox-IPis ignored: nobody else can choose the address a visit is counted from. - The secret belongs to one site (it starts with
pfp_and the site's id) and works for that site only. - PilotFox stores only a hash of it and its last four characters; it shows the secret once, when it is created.
- It lives on your server only, for example in the environment variable
PILOTFOX_PROXY_SECRET. Never put it in code that runs in the browser. - Your server must take the address from the connection it received (or from a header its own load balancer sets), never from a header your visitors can send.
Set it up
Choose the path and create the secret
In Site settings → Tracking code → Serve from your own domain, choose a path your site does not use (one or more
/ segments of letters, digits, - or _, such as /_pf, the default, or /stats/pf) and click Create secret.
Copy the secret: it is shown once. Creating or replacing it needs the editor role.
Configure your server
Use one of the recipes below; the settings page shows them with your path and PilotFox's address filled in. Each one
forwards /_pf/* to https://pilotfox.app/api/*, sets X-PilotFox-IP from the connection, adds the secret, and drops
your visitors' cookies: they belong to your site, and PilotFox reads none.
Replace the script tag
Load the script from your path:
<script defer src="/_pf/s.js" data-site="SITE_ID"></script>Verify installation recognises this tag too.
Recipes
On a self-hosted instance, replace https://pilotfox.app (and the host name pilotfox.app) with your instance's own
address.
// proxy.ts at the root of your app (Next.js 16).
// On Next.js 15 or older: name the file middleware.ts and the function middleware.
import { NextResponse, type NextRequest } from "next/server";
export function proxy(request: NextRequest) {
const { pathname, search } = request.nextUrl;
const url = new URL(pathname.replace(/^\/_pf/, "/api") + search, "https://pilotfox.app");
const headers = new Headers(request.headers);
headers.delete("cookie"); // your visitors' cookies stay on your site
headers.set("x-pilotfox-proxy-secret", process.env.PILOTFOX_PROXY_SECRET ?? "");
// The visitor's address: x-forwarded-for on Vercel, which sets it. Self-hosted: the
// header your own front proxy sets, never one your visitors can send.
headers.set("x-pilotfox-ip", request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "");
return NextResponse.rewrite(url, { request: { headers } });
}
export const config = { matcher: "/_pf/:path*" };Vercel sets x-forwarded-for to the visitor's address and overwrites any value the visitor sends. Behind your own
load balancer, use the header it sets. Plain rewrites in next.config.js can forward the path but cannot add
headers, so every visitor would have your server's address.
Replacing or removing the secret
- Replace creates a new secret and stops the old one at once: update your server right after.
- Remove turns the proxy off: requests that still carry the old secret are refused, so a stale proxy shows up as errors instead of miscounting.
- If the secret leaks, replace it. It lets its holder choose the IP address of events for that one site, nothing else: it gives no access to your data and no other site.
The same actions are available over the API with the site:update permission:
POST creates or replaces the secret (answered 201, the only
answer that contains it), DELETE removes it, and
GET (site:read) says whether there is one, with its last four
characters and creation time.
curl -X POST -H "Authorization: Bearer $PILOTFOX_API_KEY" \
-H "content-type: application/json" -d '{}' \
https://pilotfox.app/api/v1/sites/SITE_ID/proxy-secret{ "data": { "secret": "pfp_SITE_ID_…", "hint": "10Gs", "createdAt": "2026-10-27T20:26:30.761Z" } }Notes and pitfalls
| Symptom | Cause |
|---|---|
400 Invalid proxy secret | The secret is wrong, was replaced or removed, or X-PilotFox-IP is missing or not exactly one IP address. Nothing from the request is recorded. |
400 The proxy secret is for another site | The request is for another site than the secret's (another data-site, for example). |
| Every visitor in the same place, or counted as one | The proxy sends its own address, or no secret: check the X-PilotFox-IP and secret headers your server adds. |
429 from the proxy path | Too many requests: a proxy's visitors are limited one by one, within your site, and the proxy as a whole has a much larger budget. |
- Visitors are located from the address in
X-PilotFox-IP. Cloudflare's location headers are not used for requests that come through a proxy. - Your Content Security Policy then needs only your own origin
(
'self') for the script and its requests. - The settings page does not accept
/apias the path; any other path your site does not use works.