Documentation
Recipes

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:

CallPath 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:

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:

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.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.

AI agent or LLM? Read this page as markdown

On this page