# 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](https://fiveb.ar/docs/bots.md) 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](https://developers.cloudflare.com/workers/configuration/routing/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](https://vercel.com/docs/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](https://docs.netlify.com/build/edge-functions/overview/).

### 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](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html).

### 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](https://nginx.org/en/docs/http/ngx_http_realip_module.html) 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](https://caddyserver.com/docs/caddyfile/directives/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`](https://fiveb.ar/docs/script-attributes.md#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](https://fiveb.ar/docs/install.md#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](https://fiveb.ar/docs/install.md#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.
