Documentation · Safari ITP mitigation

Safari ITP mitigation on Admaxxer

Safari's Intelligent Tracking Prevention caps JS-set localStorage and JS-set first-party cookies at 7 days. Without mitigation, a visitor's first-touch UTM expires a week after their first visit on Safari — even though the pixel is configured for 365-day persistence. The Admaxxer server attaches an HTTP-only first-party cookie (__admx_first_srv) to every /api/event response that bypasses the 7-day cap and persists for the full 365 days. On day-30 return visits, the server reads the cookie, restores the original first-touch attribution, and refreshes the cookie for another year.

See the flow Why HttpOnly + first-party + Path-scoped Comparison

The 6-step flow

  1. 1 First event hits /api/event

    The visitor lands on `brand.com` for the first time, the pixel boots, and the first event (typically a pageview) posts to `/api/event` with the first-touch UTMs and click-IDs in the body.

  2. 2 Server writes the HTTP-only cookie

    On the response, the server attaches `Set-Cookie: __admx_first_srv=<base64-json>; HttpOnly; Max-Age=31536000; SameSite=None; Secure; Path=/api/event`. Because this cookie is set by the server (not JS), Safari ITP does not apply the 7-day cap.

  3. 3 Day 7 — Safari wipes JS-set localStorage and JS-set cookies

    Safari's ITP enforcement runs in the background and wipes everything the pixel wrote client-side. The HTTP-only `__admx_first_srv` survives untouched because it was server-set.

  4. 4 Day 30 — Visitor returns and a fresh visitor_id is minted

    With localStorage gone, the pixel mints a new visitor_id (V456) and fires a pageview. The body has empty `first_utm_source`, empty click-IDs, no first-touch attribution.

  5. 5 Server reads the cookie and backfills the row

    The request carries `Cookie: __admx_first_srv=<original-base64-json>`. The server decodes it, finds the original first-touch attribution from day 1, and writes the row to Tinybird with the original first_utm_source, first_utm_campaign, gclid, fbclid, etc. — fully intact.

  6. 6 Cookie freshness refreshed for another 365 days

    The response sets `__admx_first_srv` again with `Max-Age=31536000`. As long as the visitor returns at least once a year, their first-touch attribution lives forever — Safari's 7-day JS-set cap is bypassed end-to-end.

Why HttpOnly + first-party + Path=/api/event

Three constraints, each with a specific reason:

HttpOnly

Tells the browser to never expose the cookie to document.cookie or any JS read API. A malicious browser extension or compromised page-level script can't read the first-touch attribution payload — only the server (which already has it from the original event) ever sees it. This also defeats one whole category of XSS-based attribution-poisoning attacks.

First-party from admaxxer.com's perspective

Safari's 7-day cap applies specifically to JavaScript-set cookies. Server-set cookies via Set-Cookie response headers are not capped because they're considered first-party state owned by the server, not by the page's JS. This is the core mechanism — a JS-set cookie with document.cookie = '__admx_first_srv=...' would still get capped at 7 days; the same value set by the server response does not.

Path=/api/event

Defense-in-depth scoping. The cookie is only sent on requests to the ingest endpoint, never on any other Admaxxer route or any merchant-side asset. This minimises bandwidth (no unnecessary cookie overhead on every page-view's static asset fetch) and exposure (a hypothetical bug in an unrelated endpoint can't read or echo the value).

What this does NOT solve

This mitigation works for Safari (and any other browser that respects HTTP-only server-set cookies). It does not solve attribution loss on:

The strip-detection probe (per GL#361) flags these browsers in the dashboard so operators know their attribution coverage is reduced. For those visitors, identity recovers only via email-stitch on identify() — which retroactively backfills attribution against email_hash the moment a checkout email or newsletter signup arrives.

Comparison vs Triple Whale + Datafast

Triple Whale's Sonar uses email-stitch — identity recovery requires the visitor to hand over an email at checkout or signup. Datafast uses cookieless rotation + UTM persistence, which avoids the cookie cap entirely but loses click-ID granularity. Admaxxer combines all three layers: JS localStorage + JS cookie (7-day Safari) + HTTP-only server cookie (365 days Safari) + email-stitch on identify() (forever).

Safari ITP mitigation: Admaxxer vs Triple Whale Sonar vs Datafast.
Capability Admaxxer Triple Whale Datafast
Localstorage 7-day cap mitigation HTTP-only first-party cookie set by /api/event response (365 days) Sonar email-stitch (server-side identify on email hash) Cookieless rotation + UTM persistence
First-touch UTM persistence on Safari beyond 7 days Yes — restored from `__admx_first_srv` on every return visit Yes — via Sonar email-stitch Yes — via cookieless rotation
Click-IDs preserved beyond 7 days Yes — all 12 click-IDs ride along in the cookie No — click-IDs not in Sonar's stitch payload Partial — UTM only
Identifies on email needed? No — works for the full visitor lifecycle without identify() Yes — Sonar requires identify() to stitch No
HttpOnly + first-party + Path-scoped Yes — defence in depth N/A — server-stitch only Cookieless — N/A

Frequently asked

Does this require GDPR consent?
Yes — same as any other persistent identifier. The HTTP-only cookie carries first-touch attribution data (UTMs + click-IDs + a SHA-256 email hash if `identify()` was called). It's first-party (set on the merchant's domain), not third-party, so it falls under the merchant's existing consent banner — not a separate cross-site tracking disclosure. If the visitor has opted out via the JS API or the /opt-out page, the server skips writing the cookie entirely (per /documentation/consent-api → Server-side respects opt-out).
What's the cookie size?
Typically 200–400 bytes. The payload is a base64-encoded JSON object with the first-touch UTM five-tuple (source, medium, campaign, term, referrer), up to 12 click-IDs (gclid, fbclid, ttclid, msclkid, twclid, li_fat_id, epik, rdt_cid, sccid, yclid, klaviyo_id, amzn_cid), a first_landing_path string, and a 64-char SHA-256 email_hash if available. Well under the per-cookie 4 KB browser cap and the per-domain 50-cookie cap.
Can my analytics consultant read this cookie?
Not from JavaScript, no. The `HttpOnly` flag tells the browser to never expose the cookie to `document.cookie` or any JS read API. It's only readable on the server side by code receiving the request — Admaxxer's `/api/event` handler. Your consultant can see the cookie value if they have access to your Admaxxer dashboard's session-replay surface (because that runs server-side), but not via any client-side script they ship to your store.
Does this work in incognito / private browsing mode?
Partially. Incognito sessions get a fresh cookie jar that's discarded when the window closes — so the 365-day persistence is moot, but in-session attribution still works (a visitor who clicks an ad, browses your store, and converts within the same incognito window still attributes correctly). The 7-day Safari cap doesn't apply in incognito because the cookie never lives long enough to hit it.
What happens if my domain changes?
The cookie is bound to the host that set it — moving from `brand.com` to `newbrand.com` orphans the old cookie. To preserve attribution across a domain change, run a redirect from the old domain to the new one with `_admx_v` cross-domain handoff enabled (see /documentation/integrations/cross-domain-tracking). The visitor_id is propagated through the URL, the destination pixel restores the localStorage value, and the next event response sets a fresh `__admx_first_srv` on the new domain.
Why `Path=/api/event` and not `Path=/`?
Defense-in-depth. Scoping the cookie to the ingest endpoint means it's never sent on any other request — your Next.js app routes, your CDN-cached static assets, your authenticated dashboard surfaces all see clean requests with no `__admx_first_srv` overhead. It also reduces exposure: a hypothetical bug in an unrelated Admaxxer endpoint can't accidentally read or echo the cookie value.
Does this defeat Brave / Firefox-ETP / Cliqz Anti-Tracking-Protection?
No — those browsers strip `_admx_*` URL params on cross-domain navigation regardless of cookie strategy, so the cross-domain handoff fails. The strip-detection probe (GL#361) flags these browsers in the dashboard so operators know their attribution coverage is reduced. For those visitors, identity recovers only on `identify()` after they hand over an email address (e.g. checkout, newsletter signup) — which then stitches retroactively against `email_hash`.

Next steps