fivebar

Proxying

Copy page
View Markdown
Open with
Connect MCP
Cursor VS Code

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 sitePasses on to
GET /js/tally.jshttps://fiveb.ar/js/tally.js
POST /api/tallyhttps://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.

HeaderHolds
X-Forwarded-ForThe visitor’s IP address, first. Most proxies set it for you.
X-Fivebar-IPThe visitor’s IP address, where you’d rather set it yourself. It comes before X-Forwarded-For.
X-Fivebar-CountryTheir country, as a two-letter code, such as GB
X-Fivebar-RegionTheir region’s name, such as England
X-Fivebar-Region-CodeTheir region’s code, the part of its ISO 3166-2 code after the country, such as ENG
X-Fivebar-CityTheir city, such as London
X-Fivebar-ASNTheir 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.