Send events through your own domain
Proxy FounderHQ analytics through a path on your site.
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.
Before you start
You need FounderHQ analytics already working. See Getting started. This recipe changes where the browser sends events, not what it sends.
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:
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:
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:
{
"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
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/*:
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:
<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.comAn 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
- Open your site with the browser network panel filtered to
fhq. - Click something. A
POSTto/fhq/i/v2/eon your own domain returns202. - Open Analytics in FounderHQ. The event is in the live feed.
- 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.