Skip to content

Implementation Guide


Section 01 introduces the case for native identity management. This section makes it concrete: here is why you build UIAF into your application rather than bolting it on via tags.

Ad blockers cannot block your own code. Filter lists target known tracking domains. googletagmanager.com is on every list. yourdomain.com/api/collect is not — it is indistinguishable from any other API call your application makes.

Server-set cookies survive Safari ITP. A Set-Cookie header from your own server (same IP as the website) gets 400-day lifetime on Safari. JavaScript cookies get 7 days (24 hours with click ID link decoration). Your web server is, by definition, the same IP. No CNAME needed, no third-party infrastructure, no IP alignment worries.

Application-priority execution. Your server processes the request before any client-side script loads. By the time GTM initializes, your middleware has already read the existing cookie, captured UTMs and referrer from HTTP headers, and passed that context to the page.

Full HTTP access. Server-side code reads Referer, Cookie, and User-Agent headers, sets Set-Cookie, and accesses URL parameters — all before any client-side processing begins.

No external dependency. UIAF runs in your application process. If your application is up, UIAF is up. If your analytics breaks because a CDN is down, you have an external dependency problem.


The server reads HTTP headers and refreshes existing consented cookies. The client reads consent, resolves identity, requests a server-set cookie for new UIDs, and delivers payloads.


🖥️ Server-Rendered Applications (PHP/WordPress, Django, Rails, .NET, Laravel)

Section titled “🖥️ Server-Rendered Applications (PHP/WordPress, Django, Rails, .NET, Laravel)”

The most natural fit. Every page request passes through application code.

Server-side: middleware/filter. Intercept every request before the controller renders the page.

Client-side: inline script in page template. Handles storage sync, consent reading, and endpoint delivery.

// PSEUDOCODE -- Server middleware
function uiafMiddleware(request, response, next):
uid = getCookieFromRequest(request, "uiaf_uid")
consent_tier = parseConsentTier(request, UIAF_CONFIG) // see below
// T4: dormant — do not process
if consent_tier >= 4:
request.uiaf = { uid: null, pre_consent: null, consent_tier: 4 }
next()
return
// Refresh existing cookie if consent allows identity (T0/T1/T2)
if uid != null AND consent_tier <= 2:
setServerCookie(response, "uiaf_uid", uid, {
maxAge: 34560000, path: "/", secure: true,
sameSite: "Lax", httpOnly: false
})
// Server-side pre-consent capture (HTTP headers, no device storage)
pre_consent = {
utms: extractUTMs(request.query),
referrer: extractDomain(request.headers.get("Referer")),
landing_page: request.path
}
// Inject into template — client reads this after consent check
request.uiaf = { uid, pre_consent, consent_tier }
next()
// Parse consent tier from server-side signals
// Config is set once per site deployment, not per request
function parseConsentTier(request, config):
// 1. Check GPC (Sec-GPC header)
if request.headers.get("Sec-GPC") == "1" AND config.gpc_jurisdiction:
if config.gpc_strict:
return 4 // Strict: treat GPC as full opt-out
else:
return 2 // Conservative: T2 minimum (no ad data, analytics OK)
// In non-binding jurisdictions, GPC is noted in the payload but does not override tier
// 2. Read CMP cookie
consent_cookie = getCookieFromRequest(request, config.cmp_cookie_name)
// 3. No CMP cookie
if consent_cookie == null:
if config.cmp_expected:
return 3 // CMP on site but cookie not set yet = pre-consent
else:
return 0 // No CMP on this site = T0
// 4. Parse CMP cookie
try:
consent = parseCMPCookie(consent_cookie)
catch (error):
return config.corrupted_cookie_tier // Fail safe: configurable (default T3)
if consent.statistics AND consent.marketing: return 1
if consent.statistics: return 2
if not consent.statistics AND not consent.marketing: return 4 // Reject all
return 3 // Partial or ambiguous
// Site-level configuration (set once per deployment, not per request)
// config = {
// cmp_expected: true, // Does this site have a CMP?
// cmp_cookie_name: "CookieConsent", // Cookiebot. OneTrust: "OptanonConsent"
// gpc_jurisdiction: true, // Is this site subject to CA/CO/CT/TX/OR?
// gpc_strict: false, // true = GPC -> T4, false = GPC -> T2
// corrupted_cookie_tier: 3 // Tier on parse failure: 3 (pre-consent) or 4 (dormant)
// }
// PSEUDOCODE -- Inline in page template
// Server-rendered pages use a self-contained IIFE with its own state.
// The consent-transition logic is inlined (not shared with SPA module scope)
// because an IIFE's closure is isolated from other scripts on the page.
(function() {
var ctx = {{ uiaf_context_json }};
var _consent = readConsentState(); // section 07: GPC > CMP > GCM
var _identity = null;
// Consent change handler — operates on THIS closure's _consent and _identity
function handleTransition(oldConsent, newConsent) {
if (newConsent.tier >= 4) {
// attribution defaults to {first_touch: null, last_touch: null, count: 0, is_new_touch: false}
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity, page: getPageContext() },
newConsent));
clearAllUIAFStorage();
_identity = null;
return;
}
if (oldConsent.tier >= 3 && newConsent.tier <= 2) {
// Upgrade: create identity now
_identity = resolveIdentity();
if (_identity.is_new) {
fetch("/api/uiaf/cookie", { method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uid: _identity.uid }),
credentials: "include" });
}
syncAllStorage(_identity.uid);
var attribution = captureAttribution(window.location.href, newConsent);
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity,
attribution: attribution, page: getPageContext() }, newConsent));
return;
}
if (oldConsent.tier == 2 && newConsent.tier <= 1) {
// T2 -> T1: re-capture click IDs from current URL if still present
var attribution = captureAttribution(window.location.href, newConsent);
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity,
attribution: attribution, page: getPageContext() }, newConsent));
return;
}
if (oldConsent.tier <= 1 && newConsent.tier == 2) {
removeClickIdsFromStorage();
clearRetryQueue();
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity,
attribution: loadAttribution(), page: getPageContext() }, newConsent));
return;
}
if (oldConsent.tier <= 2 && newConsent.tier == 3) {
clearAllUIAFStorage();
_identity = { uid: null, session_id: getOrCreateSessionId(),
resolution_method: "ephemeral", confidence: "low", is_new: false };
// attribution defaults to {first_touch: null, last_touch: null, count: 0, is_new_touch: false}
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity, page: getPageContext() },
newConsent));
return;
}
// Same tier or unhandled transition: just send consent_update
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity,
attribution: loadAttribution(), page: getPageContext() }, newConsent));
}
// Register listener BEFORE T4 check (T4 user can re-consent)
onConsentChange(function(newConsent) {
handleTransition(_consent, newConsent);
_consent = newConsent;
});
if (_consent.tier >= 4) { return; } // T4: dormant (listener still active)
if (_consent.tier <= 2) {
_identity = resolveIdentity();
if (_identity.is_new) {
fetch("/api/uiaf/cookie", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uid: _identity.uid }),
credentials: "include"
});
}
syncAllStorage(_identity.uid);
var attribution = captureAttribution(window.location.href, _consent);
sendToEndpoint(assemblePayload(
{ event: "page_view", identity: _identity, attribution: attribution,
pre_consent: ctx.pre_consent, page: getPageContext() },
_consent
));
} else if (_consent.tier == 3) {
_identity = { uid: null, session_id: getOrCreateSessionId(),
resolution_method: "ephemeral", confidence: "low", is_new: false };
sendToEndpoint(assemblePayload(
{ event: "page_view", identity: _identity,
pre_consent: ctx.pre_consent, page: getPageContext() },
_consent
));
}
})();
PlatformMiddleware mechanismWhere to register
PHP / LaravelMiddleware classapp/Http/Kernel.php
WordPressinit hook or mu-pluginsfunctions.php or must-use plugin
DjangoMiddleware classMIDDLEWARE in settings.py
Railsbefore_actionApplicationController
.NET CoreMiddleware pipelineProgram.cs

💻 Single Page Applications (React, Vue, Angular)

Section titled “💻 Single Page Applications (React, Vue, Angular)”

SPAs handle navigation client-side after an initial page load. The challenge: the initial HTML may be a static shell with no server to set cookies.

Init on app mount, not on every route change. Resolve identity once. Hook the router for virtual pageviews and attribution changes.

Server cookie options: If a BFF serves the HTML, set Set-Cookie there. If the HTML is static, call the cookie API endpoint (/api/uiaf/cookie) on mount to get a server-set cookie, or use an edge function.

Mock Code: App Initialization + Router Hook

Section titled “Mock Code: App Initialization + Router Hook”
// PSEUDOCODE -- Runs once on app mount
// Module-scoped state, accessible to router hook and consent listener
var _identity = null
var _consent = null
function initializeUIAF():
_consent = readConsentState() // section 07: GPC > CMP > GCM
// Register consent listener BEFORE T4 check — a T4 user can re-consent
onConsentChange(function(newConsent) {
handleConsentTransition(_consent, newConsent)
_consent = newConsent
})
if _consent.tier >= 4: return // T4: dormant (but listener is active)
if _consent.tier <= 2:
_identity = resolveIdentity() // returns {uid, session_id, ...}
if _identity.is_new:
fetch("/api/uiaf/cookie", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uid: _identity.uid }),
credentials: "include"
})
syncAllStorage(_identity.uid)
installRouteHookIfNeeded() // track subsequent SPA navigations
var attribution = captureAttribution(window.location.href, _consent)
sendToEndpoint(assemblePayload(
{ event: "page_view", identity: _identity,
attribution: attribution, page: getPageContext() },
_consent
))
else:
// T3: session-only (route hook installed on upgrade if consent changes)
_identity = { uid: null, session_id: getOrCreateSessionId(),
resolution_method: "ephemeral", confidence: "low", is_new: false }
sendToEndpoint(assemblePayload(
{ event: "page_view", identity: _identity, page: getPageContext() },
_consent
))
// Consent change handler — implements section 07 state machine
function handleConsentTransition(oldConsent, newConsent):
// === DOWNGRADES ===
if newConsent.tier >= 4:
// Any -> T4: send final consent_update, then clear everything
// attribution defaults to {first_touch: null, last_touch: null, count: 0, is_new_touch: false}
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity, page: getPageContext() },
newConsent
))
clearAllUIAFStorage() // cookies, localStorage, sessionStorage, retry queue
_identity = null
return
if oldConsent.tier <= 1 AND newConsent.tier == 2:
// T1 -> T2: remove click IDs from stored attribution, keep UID
removeClickIdsFromStorage() // clear click_ids from uiaf_attribution
clearRetryQueue() // queued T1 payloads may contain click IDs
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity,
attribution: loadAttribution(), page: getPageContext() },
newConsent
))
return
if oldConsent.tier <= 2 AND newConsent.tier == 3:
// T1/T2 -> T3: delete UID and attribution from all client storage
clearAllUIAFStorage()
_identity = { uid: null, session_id: getOrCreateSessionId(),
resolution_method: "ephemeral", confidence: "low", is_new: false }
// attribution defaults to {first_touch: null, last_touch: null, count: 0, is_new_touch: false}
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity, page: getPageContext() },
newConsent
))
return
// === UPGRADES ===
if oldConsent.tier >= 3 AND newConsent.tier <= 2:
// T3/T4 -> T1 or T2: user just granted consent — full initialization
_identity = resolveIdentity()
if _identity.is_new:
fetch("/api/uiaf/cookie", { method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uid: _identity.uid }),
credentials: "include" })
syncAllStorage(_identity.uid)
var attribution = captureAttribution(window.location.href, newConsent)
// Install route hook if not already installed (SPA/SSR upgrade path)
installRouteHookIfNeeded()
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity,
attribution: attribution, page: getPageContext() },
newConsent
))
return
if oldConsent.tier == 2 AND newConsent.tier <= 1:
// T2 -> T1: re-capture click IDs from current URL if still present
var attribution = captureAttribution(window.location.href, newConsent)
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity,
attribution: attribution, page: getPageContext() },
newConsent
))
return
// Same tier or unhandled transition: just send consent_update
sendToEndpoint(assemblePayload(
{ event: "consent_update", identity: _identity,
attribution: loadAttribution(), page: getPageContext() },
newConsent
))
// Route hook installation — called on init AND on consent upgrade
var _routeHookInstalled = false
function installRouteHookIfNeeded():
if _routeHookInstalled: return
_routeHookInstalled = true
router.onRouteChange(onRouteChange) // or equivalent for your framework
// Router hook -- runs on every client-side navigation
function onRouteChange(newUrl, previousUrl):
if _consent == null OR _consent.tier >= 4: return
// Pass previousUrl as referrer (SPA navigation has no document.referrer update)
var attribution = captureAttribution(newUrl, _consent, previousUrl)
sendToEndpoint(assemblePayload(
{ event: "page_view", identity: _identity,
attribution: attribution,
page: { url: newUrl, path: extractPath(newUrl),
referrer: previousUrl, title: document.title } },
_consent
))

📄 Static Sites (Hugo, Jekyll, Astro Static, GitHub Pages)

Section titled “📄 Static Sites (Hugo, Jekyll, Astro Static, GitHub Pages)”

No server-side code. Every response is a pre-built file.

Edge function (recommended). Cloudflare Workers, Vercel Edge Middleware, Netlify Edge Functions, or AWS CloudFront Functions intercept the response and set Set-Cookie before it reaches the browser.

Client-side fallback. If edge compute is unavailable (GitHub Pages, plain S3 bucket), all logic runs in JavaScript. Safari identity persistence drops to 7 days. identity.resolution_method will report new (first visit) or cookie — downstream systems see identity.confidence: "high" or "low" indicating reduced reliability.

// PSEUDOCODE -- Edge function (Cloudflare Workers / Vercel Edge)
function handleRequest(request):
response = fetch(request) // get static page from origin
uid = getCookieFromRequest(request, "uiaf_uid")
consent_tier = parseConsentTier(request, UIAF_CONFIG) // see below
// Only refresh existing cookie if consent allows identity persistence
if uid != null AND consent_tier <= 2:
response.headers.append("Set-Cookie",
"uiaf_uid=" + uid + "; Max-Age=34560000; Path=/; Secure; SameSite=Lax")
// If no uid or consent prohibits identity: do NOT set a cookie.
// Client-side code handles UID creation after consent is verified.
return response
// Parse consent tier — see parseConsentTier above (same function, same config)

⚡ Hybrid/SSR Frameworks (Next.js, Nuxt, SvelteKit, Remix)

Section titled “⚡ Hybrid/SSR Frameworks (Next.js, Nuxt, SvelteKit, Remix)”

Best of both worlds: server middleware sets the cookie on every request, client hydration handles subsequent navigation.

Server middleware: Next.js middleware.ts, Nuxt server middleware, SvelteKit hooks.server.ts, Remix loaders.

Client hydration: After the server renders HTML, the client syncs storage and sets up router hooks.

Mock Code: Server Middleware + Client Hydration

Section titled “Mock Code: Server Middleware + Client Hydration”
// PSEUDOCODE -- Server middleware
function middleware(request):
response = NextResponse.next()
uid = request.cookies.get("uiaf_uid")
consent_tier = parseConsentTier(request, UIAF_CONFIG) // GPC + CMP cookie (see parseConsentTier above)
// T4: dormant — do not process
if consent_tier >= 4:
response.locals.uiaf_context = { uid: null, pre_consent: null, consent_tier: 4 }
return response
// Refresh existing cookie if consent allows identity (T0/T1/T2)
if uid != null AND consent_tier <= 2:
response.cookies.set("uiaf_uid", uid, {
maxAge: 34560000, path: "/", secure: true,
sameSite: "lax", httpOnly: false
})
// Do NOT create new UIDs server-side — client handles creation via cookie API
// Server-side pre-consent capture (HTTP headers, not device storage)
pre_consent = captureUTMsAndReferrer(request)
// Inject context into rendered page template (NOT response headers)
response.locals.uiaf_context = { uid, pre_consent, consent_tier }
return response
// PSEUDOCODE -- Client hydration (runs once after mount)
// Uses the same module-scoped _identity / _consent as SPA (see above).
// handleConsentTransition is shared — it already mutates _identity.
function onMount():
var ctx = readServerContext() // from server-rendered template, NOT headers
_consent = readConsentState() // section 07: GPC > CMP > GCM
// Register consent listener BEFORE T4 check (T4 user can re-consent)
onConsentChange(function(newConsent) {
handleConsentTransition(_consent, newConsent)
_consent = newConsent
})
if _consent.tier >= 4: return // T4: dormant (but listener is active)
if _consent.tier <= 2:
_identity = resolveIdentity()
if _identity.is_new:
fetch("/api/uiaf/cookie", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uid: _identity.uid }),
credentials: "include"
})
syncAllStorage(_identity.uid)
installRouteHookIfNeeded() // shared route hook (same as SPA)
var attribution = captureAttribution(window.location.href, _consent)
sendToEndpoint(assemblePayload(
{ event: "page_view", identity: _identity, attribution: attribution,
pre_consent: ctx.pre_consent, page: getPageContext() },
_consent
))
else:
// T3: session-only
_identity = { uid: null, session_id: getOrCreateSessionId(),
resolution_method: "ephemeral", confidence: "low", is_new: false }
sendToEndpoint(assemblePayload(
{ event: "page_view", identity: _identity,
pre_consent: ctx.pre_consent, page: getPageContext() },
_consent
))

The server middleware refreshes existing cookies and provides context via template injection. New UIDs get server-set cookies via the client-side cookie API call.


Section titled “🍪 Server-Side Cookie Proxy for ITP Resilience”

Three approaches for 400-day cookies on Safari.

1. Application-Level (Best). Your web server refreshes existing cookies and provides a /api/uiaf/cookie endpoint for new UIDs. Both use Set-Cookie headers from your server’s IP, giving 400-day lifetime on Safari. No extra infrastructure. This is the default approach in every pattern above.

2. Reverse Proxy Path. Route yourdomain.com/uiaf/collect through a reverse proxy to your sGTM endpoint. The browser sees your domain, your IP. Safari treats it as first-party.

// PSEUDOCODE -- reverse proxy rule
location /uiaf/collect {
proxy_pass https://your-sgtm-server.example.com/collect;
proxy_set_header X-Forwarded-For $remote_addr;
}

3. CNAME Subdomain. data.yourdomain.com CNAME to your sGTM server. Warning: Safari 16.4+ checks IP matching. If the CNAME target IP differs from the website IP, cookies are capped at 7 days. Only works when sGTM is hosted on the same infrastructure as the website.


ScenarioWhat to verifyExpected outcome
New visitor (Chrome)UID created, cookie set, identity.is_new: true in payloadCookie with 400d expiry, localStorage synced
Return visitor (Chrome)UID persistsSame UID, cookie refreshed
New visitor (Safari)Server-set cookie400d (server-set) or 7d (JS-set)
Return after 8 days (Safari, JS cookie)Cookie expiredRecovery from localStorage, identity.resolution_method: 'localstorage_recovery' in payload
Private/incognitoSession-onlyNew UID every window, no persistence
Cookie clearedRecovery from localStorageidentity.resolution_method: 'localstorage_recovery' in payload
Campaign visitAttribution capturedfirst_touch set, payload has UTMs + click IDs
Direct visit after campaignNo attribution updatelast_touch unchanged
Consent granted (T1)Full capabilityUID created, all data captured
Consent denied (T4, subsequent page load)System dormantNo UID, no storage, no endpoint call (final consent_update was sent on the page where rejection occurred)
Analytics only (T2)Click IDs strippedclick_ids: {}, data_quality: "stripped"
  • Application > Cookies: uiaf_uid present with correct format ({uuid}.{timestamp}), Secure, SameSite=Lax, HttpOnly=false, ~400-day expiry
  • Application > Local Storage: uiaf_uid matches cookie value, uiaf_attribution contains JSON with touchpoint data
  • Network: Endpoint POST body matches schema (section 04), _meta reflects current consent tier, response is 200/204
  • Console: No errors from UIAF code

  • Setting cookies via JavaScript when server-set is available. JS cookies get 7 days on Safari. Server cookies get 400. Use the server.

  • Storing raw PII in cookies or localStorage. Hash before storage (section 08). Browser storage is accessible to any script on the page.

  • Not syncing storage mechanisms. Setting only the cookie without writing to localStorage defeats the recovery hierarchy (section 02).

  • Stripping click IDs from the URL. Breaks Google Consent Mode detection, platform pixels, and downstream deduplication (section 03). Capture the value, leave the URL intact.

  • Not including consent state in endpoint payload. The consent object is required (section 04). Without it, the endpoint cannot make routing decisions.

  • Using tracking-related cookie names. Names matching _ga, _fbp, _gcl, _tracker, analytics are targeted by ad blockers (section 05). Use neutral names.

  • Firing endpoint calls before consent is resolved. Wait for the CMP to provide a definitive state before sending payloads.

  • Not handling SPA route changes. Capturing only the initial page load misses every subsequent client-side navigation.