The context envelope
Some conversions don’t fire when the user is on the page. A Stripe webhook arrives ten minutes after checkout. A subscription activates when payroll runs. A refund processes overnight. By the time you fire the server-side conversion, the browser is gone — and with it the GA4 client ID, the captured click identifiers, the consent state, and any userData you wanted to pass through.
The context envelope is the SDK’s answer: capture all of that on the browser as a plain JSON object, persist it (database row, hidden form field, x-trackbridge-context request header), and hydrate it on the server later via serverTracker.fromContext(envelope).
What’s in the envelope
Section titled “What’s in the envelope”type TrackbridgeContext = { v: 1; createdAt: number; // Unix ms clientId?: string; // GA4 _ga cookie sessionId?: string; // GA4 _ga_<containerId> cookie userId?: string; // set if you called identifyUser() clickIds: { gclid?: string; gbraid?: string; wbraid?: string }; consent: ConsentState; // snapshot at export time userData?: UserData; // pass-through, pre-normalize / pre-hash};Every field except clickIds, consent, v, and createdAt is optional. The shape is deliberately small — it round-trips through JSON.stringify losslessly with no pluggable serializers, no class instances, no functions.
The v: 1 field is a wire-format version. serverTracker.fromContext throws synchronously if it sees an unknown v — envelopes are opaque payloads, not user-editable, so a version mismatch is a programming error worth surfacing immediately.
Producing it on the browser
Section titled “Producing it on the browser”import { tracker } from '@/lib/tracker.client';
const envelope = tracker.exportContext({ userData: { email: order.customer.email, phone: order.customer.phone, },});
// Send to the server however you like:await fetch('/api/orders', { method: 'POST', body: JSON.stringify({ items: cart, trackbridgeContext: envelope }),});exportContext reads the tracker’s current state (getClickIdentifiers, getClientId, getSessionId, getConsent, plus the most recent identifyUser value) and packages it. The optional userData argument is included verbatim — pre-normalize, pre-hash. The SDK normalizes and hashes when the envelope eventually reaches trackConversion on the server, identically to the direct path.
The envelope is a snapshot at one moment. If consent changes after export, the envelope you’ve already persisted does not update.
Hydrating it on the server
Section titled “Hydrating it on the server”import { serverTracker } from '@/lib/tracker.server';
export async function POST(req: Request) { const event = await verifyStripeSignature(req); if (event.type !== 'checkout.session.completed') return new Response();
const order = await db.orders.findByStripeId(event.data.object.id); const envelope = order.trackbridgeContext; // persisted at checkout time
const bound = serverTracker.fromContext(envelope);
const result = await bound.trackPurchase({ transactionId: order.id, value: order.total, currency: order.currency, items: order.items, // clientId, gclid, gbraid, wbraid, userData all come from the envelope });
if (!result.ads.ok && !('skipped' in result.ads)) reportError(result.ads.error); return new Response();}fromContext(envelope) returns a ContextBoundServerTracker with the same five helpers (trackPurchase, trackBeginCheckout, trackAddToCart, trackSignUp, trackRefund) plus trackConversion and trackEvent. On the bound tracker, clientId and the click identifiers default to the envelope’s values — you only need to pass them per-call when you want to override.
Override rule. When both the envelope and the per-call input supply the same field, the per-call value wins. There is no deep merge. This matters most for userData: passing userData per-call replaces the envelope’s userData entirely; partial overrides require the caller to spread.
When to use the envelope
Section titled “When to use the envelope”Reach for the envelope when:
- The conversion fires meaningfully later than the user’s session (webhook, queue worker, cron).
- The conversion fires from a different process than the request handler that received the order (queue consumer in another deployment).
- You want to centralize identity capture in one place on the browser instead of threading
gclid/clientId/userDatathrough every order-related API call.
You don’t need it when:
- The server-side conversion fires inline with the HTTP request that created the order — the request can carry
clientIdandgclidas discrete fields, no envelope needed. - You only fire from the browser (Consent Mode v2 may make this unwise, but it’s a valid choice).
Versioning and forward compatibility
Section titled “Versioning and forward compatibility”The v field is a contract: any change to the envelope shape that breaks the previous reader bumps v to 2, and fromContext rejects v: 1 envelopes with a clear error. Persisted envelopes that survive across an SDK upgrade either still parse (no v change) or fail loudly (deliberate v bump).
If you persist envelopes for long periods (24 hours+), include the SDK version that produced them in your row alongside the envelope itself. It makes the rare “we upgraded the SDK and now old envelopes throw” debugging much faster.
Privacy posture
Section titled “Privacy posture”The envelope contains identifiers (clientId, clickIds) and — if you supply it — PII (userData). It is not encrypted by the SDK. Treat it the same way you treat any other PII-bearing field on the order row:
- Encrypt at rest if your database is encrypted at rest.
- Don’t log the envelope verbatim in application logs (
debug: truein development is fine; production logs should redact). - Honor user deletion requests by removing the envelope alongside the order row.
The hashed PII Google receives is derived from this envelope at fire time, not at export time. If a user requests deletion before the conversion fires, deleting the envelope prevents the hashed PII from ever reaching Google.
See also
Section titled “See also”tracker.exportContext()— browser API reference.serverTracker.fromContext()— server API reference, including theContextBoundServerTrackershape and override rules.- Stripe webhook recipe — full end-to-end example.