10. Server GTM Architecture
The server container is a Node.js app you host — clients claim requests, parse them into an event data model, tags fan out server-to-server. The full request lifecycle, preview server, and how it maps to a real production stack.
Here's the mental shift this chapter exists to make: a server container is not a configuration that lives somewhere in Google's cloud. It's an application — and you run it. Google publishes it as a Docker container image; you deploy that image on infrastructure you choose (Google Cloud, a managed host, your own VPS), give it your container's identity, and from then on the GTM console is just the editor for the config your server runs.
Once that lands, everything else in Part 3 is details.
What you actually deploy
The deployable is a Node.js application shipped as a container image
(gcr.io/cloud-tagging-10302018/gtm-cloud-image). You configure it with a
handful of environment variables, three of which matter conceptually:
CONTAINER_CONFIG— the long token the console gives you under Admin → Container Settings. It identifies which server container this instance is. The app uses it to fetch your published container — the server-side equivalent ofgtm.js, your config compiled to code — and it re-fetches periodically, so publishing in the console updates running servers within minutes, no redeploy.RUN_AS_PREVIEW_SERVER— the image runs in one of two roles (below).PREVIEW_SERVER_URL— how tagging instances find the preview instance.
Two properties of the runtime worth knowing before we open it up:
- It's stateless. Tagging instances hold no session data; any instance can serve any request. That's what makes scaling horizontal and trivial — run 1 or 30, behind any load balancer.
- It's sandboxed. Clients, tags, and variables don't run arbitrary Node.js — they run sandboxed JavaScript against a permissioned API surface (declare what hosts a template may call, what cookies it may set). Same template philosophy as Chapter 6, same security rationale, stricter environment.
Two roles, one image
┌────────────────────────┐
production traffic ──▶│ tagging server(s) │ N instances, stateless,
│ (the actual pipeline) │ scale to traffic
└───────────┬────────────┘
│ forwards debug-flagged requests
┌───────────▼────────────┐
GTM console Preview ─▶│ preview server │ EXACTLY ONE instance —
│ (debug sessions) │ holds live debug state
└────────────────────────┘The preview server is a separate deployment of the same image
(RUN_AS_PREVIEW_SERVER=true) whose only job is debugging: when you hit
Preview in the console, your test requests carry a debug marker, tagging
instances forward them to the preview server, and it streams what happened
into the console UI. It must be a single instance because a debug session is
in-memory state — the one stateful corner of the system, kept deliberately
out of the scaling path.
Clients and tags: the concept that makes it click
A web container reacts to data layer messages. A server container reacts to HTTP requests — and its defining abstraction is how it turns one into the other:
┌──────────────── server container ────────────────┐
│ │
incoming HTTP │ CLIENTS EVENT TAGS │ outbound HTTP
──────────────▶ │ (claim & parse) DATA (fan out) │ ──────────────▶
│ MODEL │
POST /g/collect │ GA4 client ──claims──▶ ┌─────────┐ ─▶ GA4 tag ─┼─▶ Measurement Protocol
en=purchase │ UA client (no) │event_name│ ─▶ Meta tag ┼─▶ Conversions API
cid=171234… │ custom client (no) │client_id │ ─▶ TikTok ─┼─▶ Events API
│ │currency │ ─▶ HTTP tag ┼─▶ anything
│ │items[]… │ │
│ └─────────┘ │
│ ◀── the claiming client also OWNS the HTTP │
│ response: status, body, Set-Cookie ────────┼─▶ response to browser
└──────────────────────────────────────────────────┘A Client is an inbound adapter. Each incoming request is offered to the
container's clients in priority order; the first one that recognizes the
format claims it. The stock GA4 client claims /g/collect requests —
which is why a browser GA4 tag pointed at your server (next chapter's
transport_url) just works. The claiming client then:
- parses the request into one or more events in a common schema —
the event data model:
event_name,client_id,currency,items, plus request metadata (IP, user agent, headers, cookies); - runs the container — triggers evaluated per event, tags fired;
- owns the HTTP response — status, body, and crucially
Set-Cookieheaders. This is the architectural slot where server-managed cookies (Chapter 11) happen: responses come from your domain's server, so the cookies it sets are first-party, HTTP-set, ITP-resistant.
A Tag is an outbound adapter. It consumes an event and makes a
server-to-server API call: the GA4 tag speaks Measurement Protocol, the Meta
tag speaks Conversions API, the generic HTTP Request tag speaks anything.
Triggers and variables work like Chapter 6 — but there are no clicks, no DOM,
no page: everything is an event, and conditions/lookups run over event
fields (event_name equals purchase) instead of page state.
The genius of the design is the decoupling in the middle: N inbound formats × M outbound destinations, joined by one event model. Your browser sends one GA4-format stream; the server fans it out to five platforms. Nobody re-instruments the site to add a destination — they add a tag. The GA4 web tag quietly became a universal transport protocol, and that is the single most practical fact about sGTM.
Clients are also templates, so the inbound side is extensible the same way: community clients accept webhook payloads (Stripe, Shopify), app events, or alternative web transports — all normalized into the same event model, all feeding the same tags.
The request lifecycle, end to end
Numbered, because in Chapter 12 you'll debug against exactly these steps:
1. browser: GA4 web tag (transport_url set) sends
POST https://pulse.shop.example/g/collect?v=2&en=purchase&cid=…
2. DNS: pulse.shop.example → YOUR server (first-party, Ch. 11)
3. container offers the request to clients in priority order
→ GA4 client recognizes /g/collect and CLAIMS it
4. client parses request → event(s) in the event data model
{ event_name:"purchase", client_id:"171234….", value:138, … }
5. triggers evaluate per event → matching tags fire
6. each tag builds + sends its vendor request, server-to-server:
GA4 tag → POST google-analytics.com/g/collect (re-signed)
Meta tag → POST graph.facebook.com/…/events (CAPI, Ch. 13)
7. client composes the HTTP response to the browser
→ 2xx, optionally Set-Cookie: FPID=… (HttpOnly, long-lived)
8. if this was a debug session: every step above streams to the
preview server → visible in the console, including the full
outbound request/response bodies of step 6Step 8 deserves an aside: the server preview shows you the actual outbound vendor calls — URL, headers, body, and the vendor's response. Client-side debugging never had this (the browser shows what left, never what the vendor did with it). It's the single biggest debugging upgrade in the whole server-side move.
Note what the browser experienced: it made one first-party request and got one response with a durable cookie. Every third-party conversation happened server-to-server, invisible to ad blockers, ITP, and DevTools alike.
Variables worth knowing server-side
The roster shifts from page-things to request-things:
- Event Data — read any field of the current event (
event_name,items.0.price); the server-side Data Layer Variable, and your workhorse. - Request header / cookie / query readers — inspect the raw claimed request when the event model doesn't carry what you need.
- Firestore Lookup — fetch a document field by key (e.g. order ID → real margin) for enrichment; GCP-only but emblematic of what "your own checkpoint" enables.
- Transformations (container feature, not a variable) — declaratively augment/edit/redact event fields before tags see them, per tag or globally. Strip PII once, centrally, instead of per-tag.
Mapping it to a real production stack
Abstract architecture, meet running system. Our Pixel Logic Server deploys exactly the pieces this chapter described, one isolated stack per client:
sGTM concept our service role
───────────────── ───────────── ─────────────────────────────
first-party edge pixel-agent the public entry on
pulse.{client-domain} — serves
the web container first-party
and routes tagging traffic
tagging server tracker-live the production container image;
claims, parses, fans out
preview server tracker-debug RUN_AS_PREVIEW_SERVER=true;
isolated so debug sessions
never touch production trafficWhen the product docs talk about a "container deployment", this is the
anatomy behind the word — and when Chapter 11 talks about first-party
domains, pulse.{client-domain} is that idea productized.
What's still missing
The architecture works, but two questions are doing silent heavy lifting:
why must this server live on a subdomain of YOUR domain (and not, say,
my-tagging.someservice.com), and what exactly should it set cookies for?
Those answers — transport_url, FPID cookies, serving gtm.js first-party,
custom loaders, and why a CNAME to the wrong place undoes half the benefits —
are Chapter 11 — First-Party Domains & Cookies.