Run your instance
First-party proxy
Ad blockers and privacy browsers drop requests to known analytics hosts. Serving misomoon from the site's own domain makes the tracker and its beacons first-party, so they count the visitors a third-party script would miss.
There are two ways to do it. Both are in the dashboard under Settings > Install, in the Proxy card, filled in for the current site.
1. Managed subdomain
The site adds one DNS record:
stats.example.com CNAME proxy.misomoon.comand enters stats.example.com in Settings. misomoon registers it as a Cloudflare for
SaaS custom hostname, Cloudflare issues the certificate (HTTP validation, usually a few
minutes once the CNAME resolves), and the site loads:
<script defer src="https://stats.example.com/m.js?s=example"></script>The browser talks to that hostname directly, so the visitor's IP and full location
arrive exactly as without a proxy. On a customer hostname the Worker only answers
/m.js, /api/event and /overlay.js; everything else is a 404.
The hostname must be a subdomain of the site's domain (not www), one per site.
If Cloudflare asks for TXT records (for example while the CNAME is proxied through
another CDN), Settings lists them next to the CNAME.
API (needs the edit permission, so an editor's session or an API key for the site works):
| Method | Path | |
|---|---|---|
GET | /api/v1/sites/:id/proxy | state; refreshes from Cloudflare until live |
PUT | /api/v1/sites/:id/proxy | { "hostname": "stats.example.com" } |
DELETE | /api/v1/sites/:id/proxy | removes the custom hostname |
Enabling it on your own deployment
- On the zone that owns the target (say
misomoon.com), enable Cloudflare for SaaS, createproxy.misomoon.com(proxied) and set it as the fallback origin. - Route customer hostnames to the Worker: add
{ "pattern": "*/*", "zone_name": "misomoon.com" }toroutesinapps/api/wrangler.jsonc, after the app's own route. - Set
CF_ZONE_IDandPROXY_CNAME_TARGETas vars andCF_API_TOKENas a secret (token permission: SSL and Certificates: Edit on that zone). - Apply the control migrations.
migrations/control/0001_init.sqlcreates theproxy_domainstable.
Without these settings GET …/proxy reports available: false, PUT answers 501
and the dashboard points to the snippets.
2. One-click deploy
Templates in templates/ deploy a ready proxy to the site owner's own account, then
they add a subdomain to it:
| Button | Then add the domain under | |
|---|---|---|
Cloudflare Worker (templates/proxy-cloudflare) | Deploy to Cloudflare | Worker → Settings → Domains & Routes |
Vercel (templates/proxy-vercel) | Deploy with Vercel | Project → Settings → Domains |
Both buttons clone from GitHub, so the repository has to be public. The Worker reads
MISOMOON_URL (default https://app.misomoon.com); the Vercel template has it in
vercel.json. The links come from deployLinks() in packages/core/src/snippets.ts.
3. Your own proxy
Map three paths under a prefix of your choice to misomoon:
| On your site | Upstream |
|---|---|
<prefix>/m.js | https://app.misomoon.com/m.js, with the query string |
<prefix>/api/event | https://app.misomoon.com/api/event |
<prefix>/overlay.js | https://app.misomoon.com/overlay.js |
m.js?s=<site id> carries the site id, and the server adds the code for the features
ticked in the site settings (tracking guide). Pass
the query string through: without it the tracker doesn't know the site and counts
nothing.
overlay.js loads only on a page the dashboard opens to draw clicks over it
(tracking guide). A proxy without it still
tracks everything, but Show on your site stays empty.
Then load <script defer src="<prefix>/m.js?s=example">. The tracker posts to
api/event next to wherever its script came from, so there is nothing else to
configure (data-api still overrides it). Pick a neutral prefix: words like
analytics, tracking or stats are on filter lists. The default is /relay.
Ready-made versions live in packages/core/src/snippets.ts (Settings shows them with
your site's values): Cloudflare Worker, Next.js, Vercel, Netlify, Nginx, Caddy and
SvelteKit.
In Next.js, withMisomoon() from @misomoon/sdk/next adds the same three rewrites to
your config (SDK guide).
Visitor IP and location
Behind a proxy the request reaches misomoon from the proxy, so the Worker reads the
visitor from headers (edgeFromRequest in packages/core/src/proxy.ts):
- IP, used only for the daily visitor hash and never stored:
x-misomoon-ip, else the firstX-Forwarded-Forhop, elseX-Real-IPand friends. - Location:
x-misomoon-country,-region,-city,-continent,-timezone,-as-org(URL-encoded), else Vercel'sx-vercel-ip-*, Netlify'sx-country, orcf-ipcountry. Without any of them, location is unknown; visitors still count. - A request counts as proxied when the forwarded IP differs from the connecting one
and the request carries a proxy's mark:
x-misomoon-ip, Vercel'sx-vercel-id, Netlify'sx-nf-request-id, Cloudflare'scf-worker, or anX-Forwarded-Hostfor another host. A bareX-Forwarded-For(corporate proxies, VPNs) is ignored. Then Cloudflare's own geo and bot score describe the proxy, so they are ignored; user-agent bot detection still runs.
| Setup | Location |
|---|---|
| Managed subdomain, Cloudflare Worker | full |
| Vercel, Next.js on Vercel, Netlify, SvelteKit on Cloudflare | what the platform forwards |
| Nginx, Caddy, other self-hosted | unknown, unless you add x-misomoon-country (e.g. from the GeoIP2 module or CF-IPCountry when Cloudflare sits in front) |
Anyone can send these headers, which only lets them misreport their own visit; the same is true of any beacon, and the site's hostname check still applies.
The Worker, SvelteKit, Nginx and Caddy snippets drop the site's cookies on the way through. Platform rewrites (Next.js, Vercel, Netlify) forward them; misomoon never reads or stores cookies.