# Serve Insights Orbit from your own domain

> Pass the script and events through your own server so fewer ad blockers stop them, and use a proxy token so each visitor's address still counts for places and unique visitors.

Source: https://docs.bifrostorbit.com/insights/developers/first-party-proxy/

Some ad blockers block analytics scripts on other domains, such as `api.bifrostorbit.com`. A first-party proxy serves the script and takes events on your own domain, for example under `/stats/`, and your server passes them on to Insights Orbit. Visitors' browsers then only talk to your website.

A proxy needs a **proxy token** to count correctly. This guide sets up both.

## Why the token matters

Insights Orbit uses the address a request comes from, for a moment, to recognise a visitor for the day and to find their country and region. Through a proxy, every request comes from your server. Without a token:

- every visit shows your server's country and region
- people who use the same browser on the same day count as one visitor, so visitors are undercounted
- if your server runs in a data centre, as most do, every visit is left out as data-centre traffic, unless an Admin switches **Leave out data centres and hosting networks** off under **Site settings**
- all your visitors share one rate limit of 120 requests a minute, so a busy website gets `429` answers and loses events

Pages, sources and events are still counted. With the token, your server passes on each visitor's address, and Insights Orbit counts as if the browser had sent the request itself. The address is used in memory and never stored, as without a proxy.

Anyone can set a header, so Insights Orbit trusts the address your server passes on only when the request also carries the site's proxy token.

## 1. Create the proxy token

1. An Admin opens the site in Insights Orbit, then **Site settings**.
2. Under **First-party proxy**, select **Create a token**.
3. Copy the line it shows, such as `INSIGHTS_PROXY_TOKEN=ins_proxy_…`. It is shown only once; Insights Orbit keeps only a scrambled form.

Each site has one token. **Replace the token** makes a new one and stops the old one at once. **Remove the token** stops it too. Creating and removing tokens is in the audit trail.

## 2. Keep it on your server

Add the token to your website's server settings, such as `.env.local` and your hosting's environment for each stage:

```sh
INSIGHTS_PROXY_TOKEN=ins_proxy_…
```

Never put it in the page, and never in a setting that starts with `NEXT_PUBLIC_`: those are written into the browser code. The site key is not a secret, but the proxy token is.

## 3. Add the proxy route (Next.js)

With the CMS Orbit SDK, one route does everything. **Install** shows it under **Serve it from your own domain (Next.js)**, with a link to create the token:

```ts
// app/stats/[...path]/route.ts
import { createInsightsProxy } from "@bifrost-orbit-cms/sdk/next";

export const { GET, POST } = createInsightsProxy();
```

It answers these addresses on your domain:

| Your address | Passed on to | Method |
| --- | --- | --- |
| `/stats/script.js` | `/insights/v1/script.js` | `GET` |
| `/stats/collect` | `/insights/v1/collect` | `POST` |
| `/stats/labels`, `/stats/label.js` | `/insights/v1/labels`, `/insights/v1/label.js` | `GET` |
| `/stats/labelling` | `/insights/v1/labelling` | `POST` |

Anything else answers `404`. It passes on `Content-Type`, `User-Agent`, `Origin`, `DNT` and `Sec-GPC`, never cookies. On `collect`, when `INSIGHTS_PROXY_TOKEN` is set and it finds the visitor's address, it adds `X-Insights-Proxy-Token` and `X-Insights-Forwarded-For`.

It reads the visitor's address from `CF-Connecting-IP`, then `X-Real-IP`, then the first entry of `X-Forwarded-For`. Your website must run behind a proxy or host that sets one of them, such as Cloudflare, Caddy, Nginx or Vercel; otherwise there is no address to pass on and visits carry your server's address.

`createInsightsProxy` takes options when the defaults don't fit:

| Option | Default |
| --- | --- |
| `token` | `INSIGHTS_PROXY_TOKEN`. Pass `null` to send no token. |
| `origin` | `INSIGHTS_ORIGIN`, or `https://api.bifrostorbit.com` when it is unset or empty. Use `https://api-stage.bifrostorbit.com` for a site in Orbit's staging. |

Both settings are read when the route first loads, so restart or redeploy the website after changing them.

## 4. Load the script from your domain

**Install** shows both versions below with your site key filled in: the SDK component for a site connected to CMS Orbit, and the script tag for any website.

```tsx
import { CmsInsights } from "@bifrost-orbit-cms/sdk/react";

<CmsInsights site={process.env.INSIGHTS_SITE} src="/stats/script.js" api="/stats/collect" />
```

Without the SDK, the script tag is:

```html
<script defer data-site="SITE_KEY" data-api="/stats/collect" src="/stats/script.js"></script>
```

Always set `data-api` (`api` in the SDK) with your own `src`. Without it, the script sends events to `/insights/v1/collect` on your domain, which your proxy doesn't answer.

With `trailingSlash: true` in `next.config.ts`, use `/stats/collect/`, so the browser isn't redirected.

`NEXT_PUBLIC_` settings are fixed when the website is built. If you build once and deploy the same build to several stages, read the site key as an ordinary server setting when the page renders, as above, rather than from `NEXT_PUBLIC_INSIGHTS_SITE`.

## Another server or framework

Any server that can pass on a request works. Serve `script.js` from `https://api.bifrostorbit.com/insights/v1/script.js`, and send each `POST` to your collect address on to `https://api.bifrostorbit.com/insights/v1/collect` with:

- the body unchanged
- `Content-Type`, `User-Agent`, `Origin`, `DNT` and `Sec-GPC` from the browser's request. Without `User-Agent`, every visit is left out as a bot.
- `X-Insights-Forwarded-For`: the visitor's address, one IP address. With a list, the first entry is used.
- `X-Insights-Proxy-Token`: the token

Answer the browser with the status and body Insights Orbit gave. The CMS Orbit SDKs for PHP, Swift and Kotlin write the script tag with your own `src` and `api`, but don't include a proxy. See [Connect Insights Orbit with the CMS Orbit SDK](https://docs.bifrostorbit.com/insights/developers/cms-orbit-sdk/).

## How Insights Orbit treats the headers

| Your request carries | What happens |
| --- | --- |
| No `X-Insights-Proxy-Token` | `X-Insights-Forwarded-For` is ignored. The connection's own address is used. |
| The right token and a valid address | The forwarded address is used, and the rate limit counts per visitor. |
| The right token, but no valid address | The connection's own address is used. |
| A wrong token, or a token after it was replaced or removed | Refused with `403` and `wrong_proxy_token`: `That proxy token is not this site's`. Nothing from that request is counted. |

A wrong token is refused rather than ignored, so a mistake shows up at once. When you replace or remove the token, update or remove `INSIGHTS_PROXY_TOKEN` on your server straight away, or every batch is refused.

## Check it works

1. Open a page of your website with the browser's developer tools on the **Network** tab.
2. Find the `POST` to `/stats/collect`. It should answer `202` with `{ "data": { "accepted": 1 } }` or more.
3. In Insights Orbit, **Right now** on the overview shows you within a few seconds, and **Places** shows your own country, not your server's.

`accepted: 0` means the batch arrived but was left out on purpose, for example because your browser sends Do Not Track. See [Troubleshooting](https://docs.bifrostorbit.com/insights/developers/troubleshooting/).

## Rewrites without a token

`insightsRewrites()` from `@bifrost-orbit-cms/sdk/next`, or rewrites in your own server, also serve the script from your domain, but can't add headers. Visits then carry your server's address, with the limits listed at the top. Use them only when the route above isn't possible, and switch the data-centre filter off under **Site settings** if your server runs in a data centre.

## Content Security Policy

Through a proxy, the browser loads and sends everything from your own domain, so `'self'` in `script-src` and `connect-src` is enough.
