# Send events through your own domain (https://www.getfounderhq.com/docs/analytics/recipes/reverse-proxy)

Serve FounderHQ analytics from a path on your own domain instead of
`i.getfounderhq.com`. About 15 minutes, and you need control of your
site's routing or web server.

<Callout type="info" title="Before you start">
You need FounderHQ analytics already working. See
[Getting started](/analytics/getting-started). This recipe changes where
the browser sends events, not what it sends.
</Callout>

## Why you would do this

Content blockers, some browser extensions, and a few corporate networks
block requests to known analytics hosts by name. Those visitors load your
page, but their events never leave the browser. You see a smaller number
than reality, and the gap grows with a technical audience.

A reverse proxy fixes it at the root. The browser calls a path on your own
domain. Your server forwards that call to FounderHQ. There is no
third-party host in the request the browser makes, so a host-based blocklist
has nothing to match.

This is a first-party path, not a disguise. Keep saying what you collect in
your privacy policy, and keep honoring consent the same way.

## How the SDK builds the URL

The `host` option is a base. The SDK appends the path itself:

| Call | Path the SDK appends |
| --- | --- |
| Send a batch of events | `/i/v2/e` |
| Fetch remote config | `/i/v1/analytics/config` |
| Mint a revenue attribution token | `/i/v2/revenue-token` |

So `host: "https://your-domain.com/fhq"` makes the browser post to
`https://your-domain.com/fhq/i/v2/e`. Your rewrite strips `/fhq` and
forwards `/i/v2/e` to `https://i.getfounderhq.com`. One rule covers all
three calls.

The gzip path adds `?compression=gzip-js` to the URL. Your proxy must pass
the query string through. Every example below does.

## Next.js

Add a rewrite in `next.config.ts`:

```ts title="next.config.ts"
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: "/fhq/:path*",
        destination: "https://i.getfounderhq.com/:path*",
      },
    ];
  },
};

export default nextConfig;
```

Then point the SDK at the path:

```ts
import { founderhq } from "@founderhq/events";

founderhq.init("fhq_pk_XXXX", {
  host: "https://your-domain.com/fhq",
});
```

The SDK only concatenates strings, so a same-origin `host: "/fhq"` also
works and needs no per-environment value.

## Vercel

If you deploy a static site or a non-Next framework on Vercel, put the same
rule in `vercel.json`:

```json title="vercel.json"
{
  "rewrites": [
    {
      "source": "/fhq/:path*",
      "destination": "https://i.getfounderhq.com/:path*"
    }
  ]
}
```

A Next.js app on Vercel should use `next.config.ts` above instead. Do not
write both.

## nginx

```nginx
location /fhq/ {
    proxy_pass https://i.getfounderhq.com/;
    proxy_set_header Host i.getfounderhq.com;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
    proxy_ssl_server_name on;
    proxy_buffering off;
}
```

The trailing slash on both `location` and `proxy_pass` is what strips
`/fhq`. Without it nginx forwards `/fhq/i/v2/e` and every request returns
`404`.

`proxy_ssl_server_name on` sends SNI to the upstream. Leave it out and the
TLS handshake fails.

## Cloudflare Worker

Route the Worker at `your-domain.com/fhq/*`:

```js
const INGEST_ORIGIN = "https://i.getfounderhq.com";

export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (!url.pathname.startsWith("/fhq/")) {
      return new Response("Not found", { status: 404 });
    }
    const target = new URL(
      url.pathname.slice("/fhq".length) + url.search,
      INGEST_ORIGIN,
    );
    return fetch(new Request(target, request));
  },
};
```

`new Request(target, request)` keeps the method, headers, and body. The
Worker runtime sets the upstream `Host` header from the URL.

## Host the snippet through the proxy too

The hosted snippet is one more path behind the same rule. Load it from your
domain and a blocklist never sees the script request either:

```html
<script src="https://your-domain.com/fhq/events.js"></script>
<script>
  founderhq.init("fhq_pk_XXXX", { host: "https://your-domain.com/fhq" });
</script>
```

## What does not change

**Allowed origins still name your own site.** A rewrite is server-side, so
the browser's request stays on your domain and its `Origin` header names
your site. If your publishable key carries an allowed-origin list, put the
origins your pages are actually served from on it:

```
https://your-domain.com
https://www.your-domain.com
```

An origin is scheme, host, and port. It has no path — add
`https://your-domain.com`, never `https://your-domain.com/fhq`. Add
`http://localhost:3000` while you develop.

**Identity is unaffected.** The SDK reads and writes its cookies in the
browser, on your own domain, and puts the IDs it needs into the event
body. They were already first-party. Proxying does not add or remove a
cookie.

**Remote config and revenue tokens follow the same base.** Once `host`
points at `/fhq`, all three calls go through the proxy. Do not cache
responses under that path: remote config is per key and the revenue token
is per visitor.

**Your server SDKs need no proxy.** `@founderhq/events-node` and crawler
tracking run on your server, where no content blocker exists. Leave their
`host` and `endpoint` at the default.

**Journey embeds do not use this path.** The web Journeys SDK accepts a
`baseUrl`, but honors only a localhost value and falls back to
`https://app.getfounderhq.com` for anything else. A Journey on your site
always calls FounderHQ directly.

## Verify

1. Open your site with the browser network panel filtered to `fhq`.
2. Click something. A `POST` to `/fhq/i/v2/e` on your own domain returns
   `202`.
3. Open Analytics in FounderHQ. The event is in the live feed.
4. Turn on a content blocker and repeat step 2. The request still returns
   `202`.

## Troubleshoot

### Every proxied request returns 404

The prefix is not being stripped. In nginx, both `location /fhq/` and
`proxy_pass https://i.getfounderhq.com/` need their trailing slash. In a
Worker, check that you slice `/fhq` off the pathname before building the
target URL.

### Requests return 401 or 403 through the proxy but work directly

Your proxy is dropping the `Authorization` header. nginx keeps it by
default; a framework middleware or a WAF rule in front of it may not.

### Gzipped batches fail but small ones succeed

The query string is being dropped. FounderHQ reads `?compression=gzip-js`
to know the body is compressed. Forward the full query string.
