Proxying
Serve the script from your own domain, and content blockers that block fiveb.ar let it through.
A content blocker that knows fiveb.ar hides its users’ visits from your stats. Served from your own domain, the script is part of your site, and those visits count. It’s worth it when many of your visitors use a blocker, as developers do, or when you’d rather your pages loaded nothing from elsewhere. It takes more than the tag alone: your platform’s recipe passes two paths on to fivebar and says who each visitor is, and then you load the script from your own domain.
How it works
Your site answers two paths and passes each on to fivebar:
| Your site | Passes on to |
|---|---|
GET /js/tally.js | https://fiveb.ar/js/tally.js |
POST /api/tally | https://fiveb.ar/api/tally |
Then load the script from your own domain. The script sends its counts to the host it was loaded from, so nothing else changes:
<script async data-domain="example.com" src="/js/tally.js"></script>Proxy those two paths and nothing else: never the dashboard, sign-in or anything else of fivebar’s.
Who the visitor is
Every count now reaches fivebar from your proxy, so your proxy has to say who the visitor is, in headers. Otherwise fivebar sees only the proxy: every visitor counts as one, in the place the proxy runs, and a proxy on a cloud network is taken for a bot, so every visit lands on the Bots page and none in your figures.
| Header | Holds |
|---|---|
X-Forwarded-For | The visitor’s IP address, first. Most proxies set it for you. |
X-Fivebar-IP | The visitor’s IP address, where you’d rather set it yourself. It comes before X-Forwarded-For. |
X-Fivebar-Country | Their country, as a two-letter code, such as GB |
X-Fivebar-Region | Their region’s name, such as England |
X-Fivebar-Region-Code | Their region’s code, the part of its ISO 3166-2 code after the country, such as ENG |
X-Fivebar-City | Their city, such as London |
X-Fivebar-ASN | Their network’s AS number, such as 2856 |
The address is the one that matters: fivebar tells visitors apart by it, as always, and never keeps it. The
rest say where the visitor is, let fivebar recognise bots by their network, and leave out the networks and
countries your site excludes. Send what your platform knows. Without them, visits still count, but with no
country, region or city, and bots are told by their user agent alone, so a visit from a hosting network counts as
a person’s. Write a name outside ASCII percent-encoded, as encodeURIComponent() does. fivebar also
reads CloudFront’s CloudFront-Viewer-* headers and Vercel’s x-vercel-ip-* ones where
they reach it.
Recipes
Each sets up the two paths and names the visitor, and passes on neither your site’s cookies nor its
Authorization header, such as a staging site’s password. Put the tag above on your pages once the
proxy is live.
Cloudflare
If your site is on Cloudflare, a Worker on your own zone can pass both paths on, and knows exactly where
each visitor is. Create a Worker with this code, and add two routes for it under its settings, such as
example.com/js/tally.js and example.com/api/tally:
export default {
async fetch(request) {
const { pathname } = new URL(request.url);
if (pathname === '/js/tally.js') return fetch('https://fiveb.ar/js/tally.js');
const cf = request.cf || {};
const headers = new Headers({
'Content-Type': 'text/plain',
'User-Agent': request.headers.get('User-Agent') || '',
'X-Fivebar-IP': request.headers.get('CF-Connecting-IP') || '',
});
const origin = request.headers.get('Origin');
if (origin) headers.set('Origin', origin);
const place = {
'X-Fivebar-Country': cf.country,
'X-Fivebar-Region': cf.region,
'X-Fivebar-Region-Code': cf.regionCode,
'X-Fivebar-City': cf.city,
'X-Fivebar-ASN': cf.asn,
};
for (const [name, value] of Object.entries(place)) {
if (value) headers.set(name, encodeURIComponent(value));
}
return fetch('https://fiveb.ar/api/tally', { method: 'POST', headers, body: request.body });
},
};It passes on only what fivebar reads. A Worker has to name the visitor in X-Fivebar-IP:
Cloudflare replaces X-Forwarded-For on a request from one of its sites to another. See Cloudflare’s
help on routes.
Vercel
Add Routing Middleware, a middleware.js at the root of your project, which names the visitor
from what Vercel knows of them. Install @vercel/functions for it:
import { geolocation, ipAddress, rewrite } from '@vercel/functions';
export const config = { matcher: ['/js/tally.js', '/api/tally'] };
export default function middleware(request) {
const { pathname } = new URL(request.url);
const headers = new Headers(request.headers);
headers.delete('cookie');
headers.delete('authorization');
const place = geolocation(request);
const visitor = {
'X-Fivebar-IP': ipAddress(request),
'X-Fivebar-Country': place.country,
'X-Fivebar-Region-Code': place.countryRegion,
'X-Fivebar-City': place.city,
};
for (const [name, value] of Object.entries(visitor)) {
if (value) headers.set(name, encodeURIComponent(value));
}
return rewrite(new URL(pathname, 'https://fiveb.ar'), { request: { headers } });
}With Next.js, put the same in middleware.js (proxy.js from Next.js 16), returning
NextResponse.rewrite(url, { request: { headers } }) in place of rewrite(). Two rewrites
in vercel.json would proxy the paths too, but Vercel doesn’t say that they pass on where the
visitor is. If Cloudflare is in front of your Vercel site, Vercel sees Cloudflare rather than the visitor, so use
the Cloudflare recipe instead. See Vercel’s help on
Routing Middleware.
Hosted anywhere but Vercel, Next.js’s own rewrites() don’t pass on the visitor’s address, so
every visitor would count as one. Proxy with whatever is in front of it instead, such as nginx or Caddy.
Netlify
Add an Edge Function, netlify/edge-functions/fivebar.js, which names the visitor from what
Netlify knows of them:
export default async (request, context) => {
const { pathname } = new URL(request.url);
if (pathname === '/js/tally.js') return fetch('https://fiveb.ar/js/tally.js');
const geo = context.geo || {};
const headers = new Headers({
'Content-Type': 'text/plain',
'User-Agent': request.headers.get('User-Agent') || '',
'X-Fivebar-IP': context.ip,
});
const origin = request.headers.get('Origin');
if (origin) headers.set('Origin', origin);
const place = {
'X-Fivebar-Country': geo.country?.code,
'X-Fivebar-Region': geo.subdivision?.name,
'X-Fivebar-Region-Code': geo.subdivision?.code,
'X-Fivebar-City': geo.city,
};
for (const [name, value] of Object.entries(place)) {
if (value) headers.set(name, encodeURIComponent(value));
}
return fetch('https://fiveb.ar/api/tally', { method: 'POST', headers, body: request.body });
};
export const config = { path: ['/js/tally.js', '/api/tally'] };A 200 rule in _redirects would proxy the paths too, but Netlify doesn’t say that it
passes on who the visitor is. See Netlify’s
help on Edge Functions.
Amazon CloudFront
In your distribution, add fiveb.ar as an origin, reached at https://fiveb.ar only, and two behaviours sending
/js/tally.js and /api/tally to it. For /api/tally, allow
POST, and choose the CachingDisabled cache policy and an origin request policy of your
own that sends only the User-Agent and Origin headers, so that fiveb.ar gets its own name
as the host and none of your site’s cookies. CloudFront adds the visitor’s address to
X-Forwarded-For. To say where they are too, have the policy also send
CloudFront-Viewer-Country,
CloudFront-Viewer-Country-Region, CloudFront-Viewer-Country-Region-Name,
CloudFront-Viewer-City and CloudFront-Viewer-ASN, which fivebar reads as it does its
own. See CloudFront’s
help on its headers.
nginx
Add two locations to your site’s server block:
location = /js/tally.js {
proxy_pass https://fiveb.ar/js/tally.js;
proxy_set_header Host fiveb.ar;
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_ssl_server_name on;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
}
location = /api/tally {
proxy_pass https://fiveb.ar/api/tally;
proxy_set_header Host fiveb.ar;
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_set_header X-Fivebar-IP $remote_addr;
proxy_ssl_server_name on;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
}The certificates’ path varies by system: it’s /etc/pki/tls/certs/ca-bundle.crt on Red Hat and
its relatives. Behind a load balancer or a CDN, $remote_addr is theirs rather than the visitor’s:
set up nginx’s real IP module to read
the visitor’s from them. With the GeoIP2 module, send X-Fivebar-Country and the rest from its
variables too.
Caddy
Add two handlers to your site’s block in the Caddyfile:
handle /js/tally.js {
reverse_proxy https://fiveb.ar {
header_up Host {upstream_hostport}
header_up -Cookie
header_up -Authorization
}
}
handle /api/tally {
reverse_proxy https://fiveb.ar {
header_up Host {upstream_hostport}
header_up X-Fivebar-IP {client_ip}
header_up -Cookie
header_up -Authorization
}
}Behind a load balancer or a CDN, list it in Caddy’s trusted_proxies, so that
{client_ip} is the visitor’s. See
Caddy’s help on reverse_proxy.
Other paths
If /js/tally.js or /api/tally is already taken on your site, proxy other paths, and
tell the script where to send its counts with data-api:
<script async data-domain="example.com" data-api="/stats/event" src="/stats/script.js"></script>Content Security Policy
Served from your own domain, the script is allowed by 'self' in script-src and
connect-src, so a Content Security Policy needs no
mention of fiveb.ar.
Check it works
Open a page of your site with your browser’s developer tools on the Network panel. The script should load from
your own domain, and a request to your own /api/tally should be answered 202:
If nothing is counted says what any other answer means. Then
open your dashboard: you should be among those on the site now, in the right country.
Check from your own connection. Where your proxy sends the visitor’s network, in X-Fivebar-ASN or
CloudFront-Viewer-ASN, as the Cloudflare and CloudFront recipes do, a visit through a VPN or from a
cloud server is a bot’s, and is on the Bots page instead. Without it, such a visit counts as yours.
If every visit counts as one visitor, all in one place, or nothing shows at all, your proxy isn’t naming the
visitor: check it sends X-Forwarded-For or X-Fivebar-IP with the visitor’s address.