Skip to content

Serve the tracker from your own domain

Some ad blockers drop requests to analytics hosts. Serving the script and the event endpoint from your own domain, on paths that do not say “analytics” or “track”, avoids that:

<script defer src="/a/s.js" data-key="wk_…" data-api="/a/e"></script>

A proxy hides your visitors from Millimetry: every event would seem to come from your server, and all your visitors would merge into one. So your proxy passes on who the visitor is, and proves it is yours with your site’s forwarding secret (fs_…). Find it under the site’s settings → Forwarding secret; it is shown once.

Header Value
X-Forwarding-Secret Your site’s fs_… secret
X-Client-IP The visitor’s IP address
X-Client-Country Optional: the visitor’s ISO country code, if your proxy knows it
X-Client-Region Optional: the visitor’s region code

Pass the visitor’s Origin, User-Agent and Content-Type headers through unchanged. Without a valid secret Millimetry ignores the other headers, so nobody else can claim to speak for your visitors. Millimetry never stores the IP address. It hashes it with a value that changes every day, and uses it to look up the country when your proxy does not send one.

Keep the secret out of your repository: in a Worker secret, an environment variable, or a file only the server can read. If it leaks, rotate it in the site’s settings. The old one stops working at once.

If your site is on Cloudflare, a Worker on the route example.com/a/* does all of this, including the visitor’s country and region.

cloudflare-worker.ts
// A Cloudflare Worker that serves the Millimetry tracker from a site's own domain (spec §12.2).
// Route it on the site's zone as `example.com/a/*`; the page then loads
// <script defer src="/a/s.js" data-key="wk_…" data-api="/a/e"></script>
// and ad blockers see a first-party script posting to a first-party path.
//
// Because the request now reaches Millimetry from Cloudflare rather than from the visitor, the
// Worker forwards who the visitor is: their address (CF-Connecting-IP) and location
// (request.cf.country / regionCode), plus the site's forwarding secret, which proves the headers
// come from the site's own proxy. Without the secret Millimetry ignores them.
//
// Set the secret with `wrangler secret put MILLIMETRY_FORWARDING_SECRET` (never in source or
// wrangler.jsonc), and run `wrangler types` in your project for the real Env type.
export interface Env {
/** The site's forwarding secret (`fs_…`), from the Millimetry dashboard. */
MILLIMETRY_FORWARDING_SECRET: string;
/** The ingest host; defaults to Millimetry's. */
MILLIMETRY_HOST?: string;
}
/** The location fields Cloudflare attaches to every request. */
interface VisitorLocation {
country?: string;
regionCode?: string;
}
/** A tracker event is a few hundred bytes; anything far larger is refused here, not buffered. */
const MAX_EVENT_BYTES = 32 * 1024;
const PASSED_THROUGH = ["content-type", "origin", "user-agent"];
async function forwardEvent(request: Request, env: Env, host: string): Promise<Response> {
const length = Number(request.headers.get("content-length") ?? Number.NaN);
if (!Number.isFinite(length) || length > MAX_EVENT_BYTES) {
return new Response(null, { status: 413 });
}
const body = await request.arrayBuffer();
if (body.byteLength > MAX_EVENT_BYTES) {
return new Response(null, { status: 413 });
}
const headers = new Headers();
for (const name of PASSED_THROUGH) {
const value = request.headers.get(name);
if (value) headers.set(name, value);
}
const cf = (request as Request & { cf?: VisitorLocation }).cf;
headers.set("x-client-ip", request.headers.get("cf-connecting-ip") ?? "");
if (cf?.country) headers.set("x-client-country", cf.country);
if (cf?.regionCode) headers.set("x-client-region", cf.regionCode);
headers.set("x-forwarding-secret", env.MILLIMETRY_FORWARDING_SECRET);
const upstream = await fetch(`${host}/v1/web/event`, { method: "POST", headers, body });
// Hand back only what the browser needs: the status and the CORS answer.
const reply = new Headers({ Vary: "Origin" });
const allowOrigin = upstream.headers.get("access-control-allow-origin");
if (allowOrigin) reply.set("access-control-allow-origin", allowOrigin);
return new Response(null, { status: upstream.status, headers: reply });
}
export async function handle(request: Request, env: Env): Promise<Response> {
const host = (env.MILLIMETRY_HOST ?? "https://e.millimetry.app").replace(/\/$/, "");
const { pathname } = new URL(request.url);
if (pathname === "/a/s.js" && request.method === "GET") {
// The script is public and cached for an hour at Millimetry's edge and here.
return fetch(`${host}/s.js`);
}
if (pathname === "/a/e" && request.method === "POST") {
try {
return await forwardEvent(request, env, host);
} catch {
// The tracker never retries; answer plainly and keep the page's console quiet.
return new Response(null, { status: 502 });
}
}
// Anything else on the route belongs to the site.
return fetch(request);
}
export default {
fetch: handle,
};
wrangler.jsonc
// An example wrangler.jsonc for the proxy Worker in cloudflare-worker.ts. Copy both into a Worker
// project on the site's Cloudflare account, adjust the route, then:
// wrangler secret put MILLIMETRY_FORWARDING_SECRET # the fs_… secret from the dashboard
// wrangler types # generates the real Env type
// wrangler deploy
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "millimetry-proxy",
"main": "cloudflare-worker.ts",
"compatibility_date": "2026-10-07",
"compatibility_flags": ["nodejs_compat"],
"routes": [{ "pattern": "example.com/a/*", "zone_name": "example.com" }],
"observability": { "enabled": true, "head_sampling_rate": 1 },
"vars": {
"MILLIMETRY_HOST": "https://e.millimetry.app"
}
}
Terminal window
wrangler secret put MILLIMETRY_FORWARDING_SECRET # paste the fs_… secret
wrangler types
wrangler deploy
location = /a/e {
proxy_pass https://e.millimetry.app/v1/web/event;
proxy_ssl_server_name on;
proxy_set_header Host e.millimetry.app;
proxy_set_header X-Client-IP $remote_addr;
include /etc/nginx/millimetry-secret.conf; # proxy_set_header X-Forwarding-Secret "fs_…";
}
location = /a/s.js {
proxy_pass https://e.millimetry.app/s.js;
proxy_ssl_server_name on;
proxy_set_header Host e.millimetry.app;
}

If nginx itself sits behind a load balancer or CDN, make sure $remote_addr is the visitor, not the load balancer. The realip module does this.

Caddyfile
handle /a/e {
rewrite * /v1/web/event
reverse_proxy https://e.millimetry.app {
header_up Host e.millimetry.app
header_up X-Client-IP {remote_host}
header_up X-Forwarding-Secret {env.MILLIMETRY_FORWARDING_SECRET}
}
}
handle /a/s.js {
rewrite * /s.js
reverse_proxy https://e.millimetry.app {
header_up Host e.millimetry.app
}
}

Open your site in a private window, then the dashboard’s live view: the visit should appear within seconds with your country. If every visit shows the same country, or visits merge into one, the proxy is not sending X-Client-IP or the secret is wrong.