Ad blockers block by hostname. Every request to truestat.io from a visitor
running one is lost before it leaves their browser. If you serve the script and
the collector from your own domain instead, those requests look like requests to
your own site, and most of them get through.
This is the single most effective thing you can do about undercounting.
Two ways to do it
Managed — you add one DNS record and we run the proxy. Nothing to configure on your server, and the certificate is issued for you.
Self-hosted — you run it, on a server you already have. Everything below the next section covers this.
Pick managed unless you already have a proxy layer you would rather use.
Managed: one DNS record
Pick a subdomain
In the app: Sites → your site → Settings → General → Managed proxy.
Enter something neutral like a.yourdomain.com or t.yourdomain.com. Avoid
analytics or tracking — those are the names blocklists look for, which
defeats the point.
It has to be a subdomain you are not already using. A hostname can hold
one CNAME record, so api.yourdomain.com will not work if it already points
somewhere.
Press Add hostname. The card then shows the record to add.
Add the CNAME record
The screen shows the exact record. At your DNS provider:
| Field | Value |
| Type | CNAME |
| Name | a — most providers want the prefix alone, not the full hostname |
| Value | the target shown on the screen |
On Cloudflare, set the record to DNS only — the grey cloud, not the orange one. A proxied record terminates at Cloudflare and never reaches us, so the certificate never issues and the setup sits at pending.
Press Check DNS
Propagation usually takes a few minutes. When it verifies we issue the certificate, which takes a few seconds more — if the screen says DNS looks right but the certificate is still coming, press it again shortly.
Replace the tag on your site
This is the step that actually turns the proxy on, and the one that gets missed — every screen in the app goes green when the certificate issues, but nothing routes through your hostname until the tag on your site changes.
The Managed proxy card shows the new tag with a copy button. Only src
is different:
<script
defer
data-website-id="ts_a1b2c3d4e5f6"
data-domain="example.com"
src="https://a.example.com/js/t.js"
></script>data-domain stays your real domain, so your data keeps landing in the
same dashboard. Wherever you pasted the original tag — your layout file, a
theme's head section, a tag manager — replace it there.
To confirm it took: load your site, open the Network tab, and check that
the request for t.js goes to your own hostname.
We forward the visitor's IP. That is the failure the next section opens with, and on the managed path it cannot happen — the hop is ours.
Removing it
Same screen, Remove hostname. The snippet reverts to loading from us directly, so paste the reverted tag on your site. You can delete the DNS record afterwards.
Self-hosted: you run the proxy
Read this first: forward the visitor's IP
A proxy that does not pass the visitor's IP address through does not degrade your numbers. It destroys them.
Here is the mechanism. Your visitors' page views arrive at your server, and your server forwards them to us. Without extra configuration, the address we see is your server's, on every single request. Every visitor then looks identical, so:
Every visitor collapses into one visitor. Your unique visitor count drops to approximately 1 and stays there.
Every visitor geolocates to your server's data centre. Your world map turns into one dot.
Page views keep counting correctly, so the failure is not obvious — you get a dashboard that looks half-working.
Every configuration below includes the IP forwarding. Do not omit it, and if you adapt one of these to a setup not listed here, that is the line to carry over.
The signature to watch for after you set this up: all your visitors suddenly in one city, and your unique visitor count flat near 1. If you see that, your proxy is dropping the IP.
What gets proxied
Two paths, always both:
| Path | What it is |
/js/t.js | The tracking script. Cacheable for a long time. |
/api/events | Where events are sent. Never cache this. |
How the script knows
If the script's own src is not on truestat.io, it sends events to
/api/events on your page's origin automatically. So for a same-origin proxy
you change one thing:
<script
defer
data-website-id="ts_a1b2c3d4e5f6"
data-domain="example.com"
src="/js/t.js"
></script>No data-api-url needed.
If you want to send events somewhere else, data-api-url accepts:
A full URL —
https://a.example.com/api/events— used exactly as given.A relative path —
/collect— resolved against your page's origin, not against the script's. This catches people out: if your script is served froma.example.combut your page isexample.com, a relativedata-api-urlsends events toexample.com, not toa.example.com.An unparseable value falls back to the default collector rather than silently sending nothing. A typo in one attribute should not zero your analytics.
If you are proxying through a separate subdomain, always use a full URL.
Choosing a subdomain
If you proxy through a subdomain rather than a path on your main site, the name matters. Blocklists target names that describe analytics.
Good: a.example.com, cdn.example.com, s.example.com
Bad: analytics.example.com, tracking.example.com, stats.example.com
Configurations
Nginx
location = /js/t.js {
proxy_pass https://truestat.io/js/t.js;
proxy_set_header Host truestat.io;
proxy_cache_valid 200 1y;
}
location = /api/events {
proxy_pass https://truestat.io/api/events;
proxy_set_header Host truestat.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}The three X- headers on /api/events are the IP forwarding. Without them,
read the warning at the top of this page again.
Caddy
example.com {
handle /js/t.js {
reverse_proxy https://truestat.io {
header_up Host truestat.io
}
}
handle /api/events {
reverse_proxy https://truestat.io {
header_up Host truestat.io
header_up X-Real-IP {remote_host}
}
}
}Caddy sets X-Forwarded-For on its own; X-Real-IP is added explicitly.
Express
const proxy = require("express-http-proxy");
// REQUIRED. Without it, req.ip is the proxy's address and every visitor
// arrives from the same place.
app.set("trust proxy", true);
app.use(
"/js/t.js",
proxy("https://truestat.io", {
proxyReqPathResolver: () => "/js/t.js",
})
);
app.use(
"/api/events",
proxy("https://truestat.io", {
proxyReqPathResolver: () => "/api/events",
proxyReqOptDecorator: (opts, req) => {
opts.headers["X-Real-IP"] = req.ip;
opts.headers["X-Forwarded-For"] = req.ip;
return opts;
},
})
);app.set('trust proxy', true) is the line people leave out.
Cloudflare Workers, Vercel Edge and similar
Any edge runtime that lets you fetch an upstream works. Forward the client address explicitly:
const upstream = new Request("https://truestat.io/api/events", request);
upstream.headers.set("X-Real-IP", request.headers.get("CF-Connecting-IP"));Use whichever header your platform provides for the real client address.
Platforms that cannot do this
Firebase Hosting cannot rewrite to an external destination. It needs a Cloud
Function running a proxy, wired in through firebase.json. It is the most
awkward setup in this list and rarely worth it.
Webflow, Framer, Squarespace, Wix, Ghost, Bubble have no way to proxy — you do not control a server. Use the default hosted script; you will lose the blocked share of your traffic, as every analytics product on those platforms does.
After you switch
Change the tag's
srcto/js/t.js(or your subdomain URL) and republish.Load a page and confirm in DevTools that
/js/t.jsreturns200from your domain andPOST /api/eventsreturns202.Wait a day, then check your unique visitor count and your world map. This is the IP-forwarding check and it is the one that matters. Views working proves nothing.
If you report AI crawlers from your server, re-copy that snippet too. Its endpoint moved to the same hostname the tag now uses.
The crawler snippet moves with the tag
The proxy changes where both halves of the install post — the tag's src,
and the endpoint the server-side crawler call sends to. Your install page
regenerates the second one automatically once the hostname is verified, so
copying step 2 again is the whole job:
// The package takes the endpoint as an option.
export const proxy = truestat("YOUR_SITE_KEY", {
endpoint: "https://a.example.com",
});Skipping it is not loud. The old call keeps working — it posts to
truestat.io, which is still a valid endpoint — so nothing breaks and no error
appears. What you lose is the reason you set the proxy up: those requests stay
on the hostname ad blockers filter. See
Server-side tracking.
Content Security Policy gets simpler
When the script is same-origin, 'self' covers both script-src and
connect-src and you need no TrueStat entry in your CSP at all. If you were
blocked by a CSP, proxying is often the shorter path to fixing it than editing
headers. See Content Security Policy.
If you proxy through a separate subdomain, that subdomain must be allowed in
both directives — 'self' does not cover it.