11. First-Party Domains & Cookies

transport_url, what genuinely counts as first-party (A records vs CNAME cloaking), FPID server-managed cookies, serving gtm.js through your own domain, custom loaders, and why proxying the tracking subdomain breaks everything.

Chapter 10's architecture runs anywhere — Google Cloud, a managed host, a VPS. But almost all of its value depends on a single decision that looks like a deployment detail and isn't: what hostname the browser talks to. The two prizes of server-side tagging — unblockable requests and durable cookies — are both properties of genuine first-partyness, and this chapter is about earning the word "genuine".

transport_url: re-pointing the browser

The browser side of the migration is almost embarrassingly small. The GA4 web tag (or Google tag) has a transport/server-container setting; set it to your tagging server's origin:

before:  POST https://www.google-analytics.com/g/collect?v=2&en=purchase…
                       └── third-party, blockable, no cookie powers
 
after:   transport_url = https://pulse.shop.example
         POST https://pulse.shop.example/g/collect?v=2&en=purchase…
                       └── first-party: your DNS, your server, your response

Same tag, same data layer, same events — different destination. Everything from Part 2 carries over untouched, which is the practical meaning of Chapter 10's "the GA4 web tag became a universal transport": you don't re-instrument the site, you re-point it.

Note what this does not re-point: other vendors' web pixels. A Meta pixel in the browser still calls facebook.com/trtransport_url is a GA4-tag setting, not a site-wide switch. The standard endgame is that the GA4 stream into your server becomes the only stream that needs to leave the browser, and Meta/TikTok/LinkedIn are fed server-side from it (Chapters 13–14, with the browser pixel kept or dropped per platform dedup strategy).

What actually counts as first-party

First-party means same registrable domain (eTLD+1): pulse.shop.example is first-party to shop.example. A subdomain is the standard choice. But the DNS record behind the subdomain decides whether browsers agree with your labeling:

how pulse.shop.example resolves        verdict
────────────────────────────────       ──────────────────────────────────
A/AAAA → infrastructure serving        genuinely first-party — full cookie
  YOUR container (your VPS, your       powers, no ITP penalty. The gold
  load balancer, dedicated IPs)        standard.
 
CNAME → a vendor's shared domain       "CNAME cloaking" — Safari resolves
  (tracker.vendor.example)             the chain (Ch. 3), sees cross-domain
                                       delegation, and caps even HTTP-set
                                       cookies at 7 days. Also: the vendor
                                       terminates TLS *as you* — a trust
                                       decision hiding in a DNS record.
 
CNAME → dedicated infra operated       the grey zone. Works, common among
  for you (your own LB hostname,       managed hosts — but Safari's rule
  per-client dedicated endpoints)      keys on the cross-domain *DNS shape*,
                                       so dedicated IPs + A records remain
                                       the maximal-durability setup; serious
                                       providers offer them for exactly this
                                       reason.

The takeaway is blunt: if Safari durability is the goal, the chain should end at an A/AAAA record to infrastructure dedicated to you. This is also why "first-party" claims from vendors deserve one question: what does the DNS look like?

Server-managed cookies: FPID

Now the response path from Chapter 10 ("the claiming client owns the HTTP response") pays out. The GA4 client has a server-managed client ID option: instead of trusting the JS-set _ga cookie as the source of identity, the server sets its own:

Set-Cookie: FPID=…; Domain=.shop.example; Max-Age=34560000;
            Secure; HttpOnly; SameSite=Lax; Path=/

Walk the attributes, because each one is doing real work:

  • HTTP-set, genuine first-party — outside ITP's JS-cookie caps. Two years means two years (well — see Max-Age below).
  • HttpOnly — page JavaScript cannot read it. That's not a limitation, it's the feature twice over: scripts can't kill it, and a compromised third-party script can't steal it.
  • Max-Age=34560000 — 400 days, and that exact number isn't arbitrary: it's the maximum cookie lifetime Chrome enforces (per the updated cookie spec). Ask for more and Chrome clamps to 400 days anyway, so 400 is the honest ceiling. (It's also why our own stack pins precisely this value — set the cap, get the cap.)
  • The GA4 client handles the translation transparently: incoming requests carry FPID (the browser attaches it automatically — same domain), outgoing Measurement Protocol hits carry a client ID derived from it. GA4 reports don't know or care.

Mechanically, identity now lives in a place where the browser's anti-tracking machinery has agreed not to look — because HTTP-set first-party cookies are what login sessions are made of, and that's a hostage browsers can't shoot.

Limits, stated plainly: still one browser on one device (no cross-device magic), still gone if the user clears site data, still subject to consent (a cookie is a cookie — Chapter 17). Durable ≠ omniscient.

Serving the container itself first-party

One leak remains: the page still loads gtm.js from googletagmanager.com — a hostname firmly on every blocklist (Chapter 3), and if the container never loads, there's nothing to transport. The server container closes this too: stock clients can serve the Google scripts through your domain — the snippet's src becomes

https://pulse.shop.example/gtm.js?id=GTM-XXXXXXX

and your tagging server fetches the script from Google, then serves it as a first-party asset — the GTM Web Container client handles this (and since sGTM v3.2.0 it's the one place all Google JS serving lives, gtag.js included). Hostname problem solved.

Custom loaders: the second half of the trick

Blocklists don't only match hostnames — they match paths. /gtm.js and /g/collect are themselves on filter lists, so a first-party domain with stock paths still loses requests to path rules. The countermeasure is the custom loader: serve the loader script and collection endpoints under non-standard, per-site paths —

https://pulse.shop.example/cdn/a7f3k2.js        ← the loader
https://pulse.shop.example/a7f3k2/data           ← the event endpoint

— so neither hostname nor path matches a list. This is precisely what Stape's "Custom Loader" power-up sells (Chapter 16), and in our own stack it's pixel-agent's job: each client's edge serves the web container under client-specific stealth paths, so the loader survives list-based blocking.

Honesty paragraph, as always: this is an arms race. Filter lists update; heuristic blockers profile script behavior, not just URLs. First-party serving plus custom loading recovers a large, real share of blocked measurement — and nothing in this space is permanent. Build for graceful degradation, not for "100% capture", and re-audit quarterly.

Google's own flavor: Tag Gateway

Worth knowing the official cousin: Google tag gateway for advertisers (generally available since May 2025) gets first-party serving without an sGTM container — your CDN (Cloudflare at launch; Akamai, Fastly, CloudFront since) or a GCP load balancer intercepts Google-tag requests on your own domain and relays them to Google. It's the serving benefit productized for Google tags specifically — useful on its own, but note what it doesn't give you: no checkpoint, no server-set cookies under your control, no multi-vendor fan-out. A complement to a server container, not a replacement.

The proxy trap: why Cloudflare breaks it

The most common self-inflicted wound in production sGTM, and our support inbox's old friend: someone "protects" the tracking subdomain behind a proxying CDN (Cloudflare orange-cloud), and three things degrade at once:

  1. Preview/debug dies. The console's server preview is a live stream from the preview server (Chapter 10) over long-held connections. Buffering proxies hold and batch those responses — the debug UI hangs or shows nothing. If server preview "just spins", check the DNS cloud color before anything else.
  2. The client IP disappears. Your server now sees the proxy's IP unless it's configured to trust forwarding headers. Geo reporting and platform match quality (IP is a match signal — Chapter 13) quietly degrade.
  3. Caching/bot rules eat events. A CDN that caches 200s or "challenges" suspicious POSTs is making per-request decisions about your measurement pipeline.

Hence the rule (it's Critical Rule #5 in our own runbook): tracking subdomains are DNS-only. No proxy, no cache, no bot-management — TLS and a straight line to the tagging server. A CDN in front of your site is fine; in front of your collector, it's a man in the middle you hired yourself.

The healthy-setup checklist

  • Tracking host is a subdomain of the site's own registrable domain
  • DNS terminates at an A/AAAA record to dedicated infrastructure (or you've consciously accepted the CNAME grey zone)
  • No proxying/caching/bot-layer on the tracking host — DNS-only
  • transport_url set on the GA4 web tag; events arriving at /g/collect (verify in server preview)
  • Container scripts served first-party — ideally via custom loader paths
  • Server-managed client ID (FPID) enabled; Set-Cookie visible in the response with HttpOnly + Max-Age=34560000
  • Client IP and User-Agent correctly forwarded to the container (check event data in preview — geo should be the visitor, not the datacenter)

With the pipe built and the cookies durable, it's time to watch one event travel the whole road and learn to debug every hop: Chapter 12 — GA4 End to End.