> ## Documentation Index
> Fetch the complete documentation index at: https://heedeo-e1965c.manicule.dev/llms.txt
> Use this file to discover all available pages before exploring further.
> For every page in one file, fetch https://heedeo-e1965c.manicule.dev/llms-full.txt (large).
> To search and read these docs as tools, connect the MCP server at https://heedeo-e1965c.manicule.dev/api/mcp.
> Every agent surface: https://heedeo-e1965c.manicule.dev/.well-known/llms-manifest.json

# 2. Serve Heedeo from your own domain (do this before you go live) (/serve-heedeo-from-your-own-domain)

<!-- agent-signals: reading_time_min: 10 · est_tokens: 3417 · updated: 2026-11-11 -->
Related: [Integrating Heedeo](/index.md), [Connect your app](/connect-your-app.md), [1. Quick start](/quick-start.md), [3. One server call: evaluate or log](/one-server-call.md), [4. The fields to act on](/the-fields-to-act-on.md), [5. Decide: score, rules or watch](/decide.md)

Do this before you go live. From your own domain, ad blockers and privacy browsers like Safari and Brave don't block Heedeo, and returning visitors keep their cookie.

Three ways, in this order, each set up from the Domains page (`https://app.heedeo.com/review/domains`):

* **Cloudflare proxy integration** (recommended), for a site Cloudflare already serves, with no code: Heedeo deploys a Worker to your zone. Full cookie life in every browser. About 2 minutes.
* **Your own server**: a route in your own app forwards two paths, with the package's proxy. Full cookie life in every browser. Served by your own app. When your pages have no server of their own, that route goes on your API (section 3, A single-page app, a hosted sign-in and a separate API).
* **Custom subdomain**, for any other site, with no code: point one subdomain at Heedeo with a CNAME. Works with any DNS provider. Safari limits its cookie to 7 days.

The Cloudflare proxy integration and the custom subdomain serve two paths under your workspace's path prefix and nothing else: `<prefix>/i.js` and `<prefix>/v1/identify`. The prefix looks like `/k9m2x` and is shown on the Domains page. A workspace made before the page has `/hd`, and Regenerate there gives it a random one.

Only the `_idd` cookie goes up to Heedeo; your own session cookies never reach us.

Both of those fail open. While a host is pending, removed, over quota or pointed wrong, the script does not load or `identify()` rejects, and your page sends no token, so your server asks for verification where your app has a step (section 3).

## Cloudflare proxy integration

1. On the Domains page, paste your Cloudflare account id and an API token with three permissions: Zone › Zone › Read, Account › Workers Scripts › Edit and Zone › Workers Routes › Edit.
2. Press List zones and pick the zone your pages are on.
3. Press Deploy. Heedeo deploys a Worker named `heedeo-k9m2x` (your prefix) into your account, on the route `*customer.com/k9m2x/*`.
4. The page shows the endpoint on the zone, `https://customer.com/k9m2x`. The Worker answers the same paths on every host under it. Use the host your pages are on, like `https://www.customer.com/k9m2x`, so that your page and the script are one origin.

About the token:

* Give it those three permissions and nothing more. Heedeo checks a token with Cloudflare before it keeps or uses it, and refuses one that can do more (Cloudflare's "Edit Cloudflare Workers" template can).
* Workers Scripts › Edit is account-wide in Cloudflare, so the token can change any Worker in that account. If that is too much, put the zone in an account of its own.
* Heedeo keeps the token sealed and shows it nowhere. It uses the token only for this Worker, and only when you press a button on the Domains page: to list your zones, deploy it, redeploy it, apply an update and delete it.

When we ship a new version of the Worker, the Domains page says so and offers Apply the update. Heedeo never changes the Worker in your account on its own, and the one you have keeps serving until you apply it.

On Workers Free the Worker serves 100,000 requests a day. Past that, Cloudflare skips it, and your page sends no token, so your server asks for verification where your app has a step (section 3).

## Your own server

A route in your own app forwards Heedeo's two paths under `/hd`. Served by your own app, Heedeo keeps the visitor cookie for its full life in every browser.

Each guide below does what the package's `heedeo.proxy` does:

* `GET /hd/i.js` goes to `https://app.heedeo.com/i.js` and `POST /hd/v1/identify` to `https://app.heedeo.com/v1/identify`, with the query string. Anything else under `/hd` answers 404 and never reaches Heedeo.
* It adds `authorization: Bearer <your secret API key>`, from `HEEDEO_SECRET_KEY`, and `x-heedeo-client-ip` with the visitor's IP, from a source your host makes trustworthy. Heedeo believes that IP only beside the key.
* It sends up only the browser's `origin`, `user-agent`, `content-type`, `accept-language`, client hints (`sec-ch-ua` and the `sec-ch-ua-*` headers), `if-none-match` and the `_idd` cookie, never your session's. It passes back `set-cookie`, `content-type`, `cache-control`, `etag`, `accept-ch` and the `access-control-*` headers, and a `304` with no body.

Then load Heedeo from the route, `createHeedeo({ publishableKey, path: "/hd" })` or the tag from `/hd/i.js`, and keep both paths public, with no sign-in. After you deploy, `https://your-site.com/hd/i.js?key=pk_...` answers JavaScript. A route on `heedeo.proxy` answers 503 until its `clientIp` returns an address.

### Next.js

A route handler, with `lib/heedeo.ts` and its `clientIp` from the package quickstart (`https://app.heedeo.com/docs/sdk`, Serve Heedeo from your own domain):

```ts
// app/hd/[...path]/route.ts
import { heedeo } from "../../../lib/heedeo";

export const GET = heedeo.proxy;
export const POST = heedeo.proxy;
```

Or in `proxy.ts` at the project root (on Next 15, `middleware.ts` exporting `middleware`):

```ts
// proxy.ts
import type { NextRequest } from "next/server";
import { heedeo } from "./lib/heedeo";

export function proxy(request: NextRequest) {
  return heedeo.proxy(request);
}

export const config = { matcher: "/hd/:path*" };
```

Don't use a `rewrites` entry in `next.config`: it cannot add the secret API key, so Heedeo ignores the visitor's IP.

### Vercel

Any other framework on Vercel: Routing Middleware, in `middleware.ts` at the project root.

```ts
// middleware.ts
import { Heedeo } from "@heedeo/sdk";
import { ipAddress } from "@vercel/functions";

const heedeo = new Heedeo({
  secretKey: process.env.HEEDEO_SECRET_KEY,
  clientIp: (request) => ipAddress(request) ?? null,
});

export default function middleware(request: Request) {
  return heedeo.proxy(request);
}

export const config = { matcher: "/hd/:path*" };
```

### Netlify

An Edge Function. `context.ip` is the visitor's IP.

```ts
// netlify/edge-functions/heedeo.ts
import { Heedeo } from "@heedeo/sdk";
import type { Config, Context } from "@netlify/edge-functions";

export default (request: Request, context: Context) =>
  new Heedeo({
    secretKey: Netlify.env.get("HEEDEO_SECRET_KEY"),
    clientIp: () => context.ip,
  }).proxy(request);

export const config: Config = { path: "/hd/*" };
```

### nginx

```nginx
# In the http block: Heedeo's cookie alone goes up.
map $cookie__idd $heedeo_cookie {
    "" "";
    default "_idd=$cookie__idd";
}

# In your server block.
location /hd/ {
    proxy_pass_request_headers off;
    proxy_set_header Authorization "Bearer ${HEEDEO_SECRET_KEY}";
    proxy_set_header X-Heedeo-Client-Ip $remote_addr;
    proxy_set_header Cookie $heedeo_cookie;
    proxy_set_header Origin $http_origin;
    proxy_set_header User-Agent $http_user_agent;
    proxy_set_header Content-Type $content_type;
    proxy_set_header Accept-Language $http_accept_language;
    proxy_set_header Sec-CH-UA $http_sec_ch_ua;
    proxy_set_header Sec-CH-UA-Platform $http_sec_ch_ua_platform;
    proxy_set_header Sec-CH-UA-Mobile $http_sec_ch_ua_mobile;
    proxy_set_header Sec-CH-UA-Platform-Version $http_sec_ch_ua_platform_version;
    proxy_set_header Sec-CH-UA-Model $http_sec_ch_ua_model;
    proxy_set_header Sec-CH-UA-Arch $http_sec_ch_ua_arch;
    proxy_set_header Sec-CH-UA-Bitness $http_sec_ch_ua_bitness;
    proxy_set_header Sec-CH-UA-Full-Version-List $http_sec_ch_ua_full_version_list;
    proxy_set_header If-None-Match $http_if_none_match;
    proxy_ssl_server_name on;
    client_max_body_size 64k;

    location = /hd/i.js {
        limit_except GET { deny all; }
        proxy_pass https://app.heedeo.com/i.js;
    }
    location = /hd/v1/identify {
        limit_except POST OPTIONS { deny all; }
        proxy_pass https://app.heedeo.com/v1/identify;
    }
    return 404;
}
```

The official nginx image fills `${HEEDEO_SECRET_KEY}` from the environment for files in `/etc/nginx/templates`; elsewhere, fill it at deploy. Behind a load balancer, `$remote_addr` is the balancer: add `set_real_ip_from <its address range>;` and `real_ip_header X-Forwarded-For;` so it is the visitor.

### Express

```js
// Heedeo's two paths under /hd, with your secret API key and the visitor's IP; 404 for the rest.
const HEEDEO = "https://app.heedeo.com";
const PATHS = { "/i.js": ["GET"], "/v1/identify": ["POST", "OPTIONS"] };
const UP = ["origin", "user-agent", "content-type", "accept-language", "sec-ch-ua", "sec-ch-ua-platform",
  "sec-ch-ua-mobile", "sec-ch-ua-platform-version", "sec-ch-ua-model", "sec-ch-ua-arch",
  "sec-ch-ua-bitness", "sec-ch-ua-full-version-list", "if-none-match"];
const BACK = ["content-type", "cache-control", "etag", "accept-ch"];

app.set("trust proxy", 1); // req.ip is the visitor behind one load balancer; match your setup

app.use("/hd", express.raw({ type: () => true, limit: "64kb" }), async (req, res) => {
  if (!PATHS[req.path]?.includes(req.method)) return res.sendStatus(404);
  const headers = {
    authorization: `Bearer ${process.env.HEEDEO_SECRET_KEY}`,
    "x-heedeo-client-ip": req.ip.replace(/^::ffff:/, ""),
  };
  for (const name of UP) if (req.headers[name]) headers[name] = req.headers[name];
  const idd = /(?:^|;\s*)_idd=([^;]*)/.exec(req.headers.cookie ?? "")?.[1];
  if (idd) headers.cookie = `_idd=${idd}`;
  try {
    const up = await fetch(HEEDEO + req.url, {
      method: req.method,
      headers,
      body: req.method === "POST" ? req.body : undefined,
      redirect: "manual",
      signal: AbortSignal.timeout(3000),
    });
    for (const [name, value] of up.headers)
      if (BACK.includes(name) || name.startsWith("access-control-")) res.set(name, value);
    for (const cookie of up.headers.getSetCookie()) res.append("set-cookie", cookie);
    res.status(up.status).send(Buffer.from(await up.arrayBuffer()));
  } catch {
    res.sendStatus(502);
  }
});
```

### Hono

With `trustedClientIp` from the package quickstart:

```ts
import { Heedeo } from "@heedeo/sdk";
import { trustedClientIp } from "./trusted-client-ip";

const heedeo = new Heedeo({ secretKey: process.env.HEEDEO_SECRET_KEY, clientIp: trustedClientIp });

app.all("/hd/*", (c) => heedeo.proxy(c.req.raw));
```

## Custom subdomain

Safari limits its cookie to 7 days on a custom subdomain. The Cloudflare proxy integration and your own server keep it for its full life, so use a custom subdomain when neither fits.

1. On the Domains page, type a hostname on the domain your pages are on, like `id.customer.com`. It must have three parts and be on a domain you own, not a shared one like `vercel.app`. It cannot start with a word blocking lists match, such as `track` or `analytics`. A workspace can have up to five.
2. Add one record at your DNS provider: `id.customer.com CNAME connect.heedeo.com`.
3. Cloudflare then issues the certificate, and the hostname turns from Pending to Active. Check asks at once; Heedeo also asks every hour.
4. Your endpoint is `https://id.customer.com/k9m2x`.

Your page and the host are the same site but not the same origin. The script posts with credentials, and once the hostname is live identify answers CORS for your domain's origins, `https://customer.com` and `https://*.customer.com`, and refuses every other origin. Until then nothing changes for pages that already work. Other pages that identify go under Origins on the same page.

* A hostname whose CNAME is not in within 7 days is released.
* If another workspace added your hostname first and never set it up, the page gives you a TXT record to add (`_heedeo.id.customer.com`). With it in your DNS, add the hostname again and it is yours.
* When you remove a hostname, take its CNAME out of your DNS too. For 30 days the hostname stays reserved to your workspace; after that, anyone could add it and be served what still reaches it.

## Load Heedeo from your domain

One rule for the Cloudflare proxy integration and the custom subdomain: the host shares a registrable domain with your pages. A hostname on `customer.com` serves pages on `customer.com` and its subdomains, not pages on `customer.io`.

What each way gives:

* Cookie life: the Cloudflare proxy integration and your own server keep the visitor cookie for its full life in every browser. On a custom subdomain, Safari limits it to 7 days.
* Ad blockers: either way the script comes from your own host under your own prefix, not from a Heedeo host or a fixed path, so a list that names Heedeo does not match it. A blocker that reads CNAMEs (uBlock Origin on Firefox can) sees that your hostname points at Heedeo; the Cloudflare way has no CNAME. When a blocker stops the script, the page sends no token, and your server asks for verification where your app has a step (section 3).

Then load Heedeo from the endpoint. The Install card on the Domains page shows both lines with your key and endpoint filled in. Add no proxy route.

```html
<script src="https://id.customer.com/k9m2x/i.js?key=pk_YOUR_PUBLISHABLE_KEY" async></script>
```

Or with the package (see the [package quickstart](https://app.heedeo.com/docs/sdk)):

```ts
import { createHeedeo } from "@heedeo/sdk/browser";

export const heedeo = createHeedeo({ publishableKey, endpoint: "https://id.customer.com/k9m2x" });
```

`endpoint` needs `@heedeo/sdk` 0.1.0-pilot.12 or later. Section 1's `identify()` holds as written, with this `src` in the tag.

Installed from `heedeo.dev` or your own route before adding the domain? Change only the browser line, the script's `src` or `createHeedeo`'s `endpoint`; the server's event call needs nothing new, and a route can go once the endpoint answers. The Domains page shows the switch with your endpoint filled in.
