PilotFoxDocs

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 requestsYour server forwards it to
/_pf/s.jshttps://pilotfox.app/api/s.js (the script)
/_pf/c/SITE_IDhttps://pilotfox.app/api/c/SITE_ID (the site's settings)
/_pf/e, /_pf/ihttps://pilotfox.app/api/e, /api/i (events, identify)
/_pf/r.js, /_pf/rec/SITE_IDhttps://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-IP is 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
// 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

SymptomCause
400 Invalid proxy secretThe 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 siteThe request is for another site than the secret's (another data-site, for example).
Every visitor in the same place, or counted as oneThe proxy sends its own address, or no secret: check the X-PilotFox-IP and secret headers your server adds.
429 from the proxy pathToo 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 /api as the path; any other path your site does not use works.

On this page