Skip to content

Identity Management


The User Identifier (UID) is the core persistent token assigned to every visitor:

{uuid_v4}.{unix_timestamp_seconds}

Example:

f81d4fae-7dec-11d0-a765-00a0c91e6bf6.1647291600

Opacity. The UID reveals nothing about the user. No IP address, no device fingerprint, no PII. It is a random string with a timestamp. Even if intercepted, it provides no information about the individual beyond the fact that they visited the site on or after the timestamp date.

The total size of a UID is approximately 50 bytes — well within any storage mechanism’s limits.


Storage mechanisms are ranked from most resilient to least resilient. The system writes to ALL available mechanisms simultaneously and reads in this priority order:

Section titled “1. Server-Set HTTP Cookie (Set-Cookie response header)”

The most resilient storage mechanism available. A cookie set via the HTTP Set-Cookie response header from a same-IP first-party server survives Safari ITP’s 7-day JavaScript cookie cap and achieves the maximum 400-day lifetime across all major browsers.

This is the primary reason native platform integration matters. A third-party JavaScript tag cannot set server-side cookies — only the website’s own backend can issue a Set-Cookie header from a same-IP origin.

Section titled “2. Client-Set Cookie (document.cookie in JavaScript)”

Falls back here when server-side cookie setting is not available (e.g., static sites, SPAs without server rendering). Lifetime varies significantly by browser:

  • Chrome, Firefox, Edge: 400 days (same as server-set)
  • Safari, Brave: 7 days maximum
  • Safari with link decoration from a classified tracker: 24 hours

Persistent in Chrome, Firefox, and Edge with no time-based expiration. Safari deletes localStorage after 7 days without user interaction on the site. Not sent with HTTP requests, so it cannot replace cookies for server-side identity resolution — but it serves as a recovery mechanism when cookies are cleared.

Per-tab, session-only storage. Cleared when the tab closes. Always available across all browsers, but provides no cross-session persistence. Its role is last-resort recovery within a single session (e.g., if cookies and localStorage are both cleared mid-session by an extension or privacy tool).


Every page load executes identity resolution: determine whether this visitor already has a UID, recover it if possible, or generate a new one.

This function has two forms:

  • Server-side: resolveIdentity(request) — reads cookie from HTTP request, returns uid or null. The server-side form only READS. It never creates UIDs or writes cookies. Cookie refreshing is handled by the server middleware (see 09-implementation-guide.md).
  • Client-side: resolveIdentity() — no arguments. Reads cookie, localStorage, sessionStorage in priority order. Generates a new UID if none found. Returns {uid, session_id, is_new, resolution_method, confidence}. For new UIDs, the client requests a server-set cookie via the /api/uiaf/cookie endpoint.
// PSEUDOCODE — Client-side resolveIdentity (no arguments)
function resolveIdentity():
uid = null
resolution_method = null
is_new = false
// Session ID: always generated, stored in sessionStorage
session_id = sessionStorage.getItem("uiaf_session_id")
if session_id is null:
session_id = generateUUIDv4()
sessionStorage.setItem("uiaf_session_id", session_id)
// 1. Check cookie (could be server-set or client-set — we can't distinguish)
uid = getClientCookie("uiaf_uid")
if uid is not null:
resolution_method = "cookie" // not "server_cookie" — client can't know who set it
return { uid, session_id, resolution_method, is_new: false, confidence: "high" }
// 2. Check localStorage
uid = localStorage.getItem("uiaf_uid")
if uid is not null:
resolution_method = "localstorage_recovery"
return { uid, session_id, resolution_method, is_new: false, confidence: "medium" }
// 3. Check sessionStorage
uid = sessionStorage.getItem("uiaf_uid")
if uid is not null:
resolution_method = "sessionstorage_recovery"
return { uid, session_id, resolution_method, is_new: false, confidence: "medium" }
// 4. Nothing found — new identity
uid = generateUUIDv4() + "." + currentUnixTimestamp()
return { uid, session_id, resolution_method: "new", is_new: true, confidence: "low" }
// NOTE: This function does not emit events. The caller sends the page_view
// payload with identity.is_new and identity.resolution_method set, which is
// sufficient for the endpoint to detect new visitors and recoveries.
// If separate identity_init/identity_recover events are needed, the caller
// can check is_new and resolution_method and send additional payloads.

The resolution_method field is included in every endpoint payload (see 04-endpoint-schema.md). This enables downstream analysis of how often identities are recovered vs. created fresh, and which storage mechanisms are doing the recovery work — a direct measure of browser storage policy impact.


AttributeValueWhy
Nameuiaf_uidConsistent, recognizable, unlikely to collide with existing cookies
SecuretrueHTTPS only — prevents cookie interception over unencrypted connections
SameSiteLaxPrevents CSRF by blocking cross-site subrequests. Allows normal top-level GET navigation (user clicking a link to your site)
HttpOnlyfalseNeeds JavaScript access for localStorage sync, SPA navigation, and client-side recovery
Max-Age34560000 (400 days)Maximum permitted by RFC 6265bis, enforced by all modern browsers. Browsers silently cap values exceeding this
Path/Available across the entire site
Domainsite-specificSet explicitly (e.g., .example.com) for subdomain sharing. Omit entirely for single-domain sites

This is a deliberate tradeoff. HttpOnly cookies cannot be read by JavaScript, which means:

  • localStorage synchronization is impossible — the client cannot read the cookie value to write it to localStorage as a backup.
  • SPA frameworks that handle navigation client-side cannot access the UID for event payloads without an additional server round-trip.
  • Recovery from localStorage/sessionStorage back to cookie requires knowing the UID value, which HttpOnly prevents.

The security cost: a non-HttpOnly cookie is readable by any JavaScript executing on the page, including injected scripts from an XSS vulnerability. The UID is a random identifier with no inherent value (it is not a session token granting access to anything), which limits the impact of exposure. However, if your platform already sets the UID server-side on every request (e.g., via middleware), and you do not need client-side recovery, setting HttpOnly to true is the more secure choice.

The research in 01-browser-tracking-prevention.md recommends __Host- prefix with HttpOnly for maximum security. This spec departs from that recommendation in favor of cross-storage synchronization, which is the mechanism that recovers identities when cookies are cleared. Implementers should evaluate this tradeoff for their specific architecture.


Sessions are distinct from persistent identity. A session represents a single continuous period of activity; the persistent UID spans many sessions.

Session ID format: Random UUID v4 (no timestamp needed — sessions are ephemeral by nature).

Storage: sessionStorage only. Sessions must not persist beyond the tab lifecycle.

Session lifecycle:

  1. On page load, check sessionStorage for uiaf_session_id.
  2. If absent: generate a new session ID, store in sessionStorage, mark this as a new session.
  3. If present: this is a session continuation.

Critical rule: Never overwrite the persistent UID when starting a new session. The session ID and the persistent UID are independent. Both are included in every endpoint payload — the UID for cross-session identity, the session ID for within-session analysis.

// PSEUDOCODE — Adapt to your platform
function resolveSession():
session_id = sessionStorage.getItem("uiaf_session_id")
is_new_session = false
if session_id is null:
session_id = generateUUIDv4()
sessionStorage.setItem("uiaf_session_id", session_id)
is_new_session = true
return { session_id, is_new_session }

When the UID is found in ANY storage mechanism, immediately write it back to ALL other mechanisms. This keeps every mechanism current and maximizes the probability of recovery on the next visit.

// PSEUDOCODE — Adapt to your platform
function syncAllStorage(uid):
// Client-side cookie (backup for when server cookie is unavailable)
setClientCookie("uiaf_uid", uid, {
maxAge: 34560000,
path: "/",
secure: true,
sameSite: "Lax"
})
// localStorage (survives cookie clearing on most browsers)
try:
localStorage.setItem("uiaf_uid", uid)
catch (error):
// localStorage may be unavailable (private browsing, storage full, disabled)
// Fail silently — other mechanisms still function
pass
// sessionStorage (last resort for current session)
try:
sessionStorage.setItem("uiaf_uid", uid)
catch (error):
pass

The try/catch blocks are not defensive programming theater. localStorage and sessionStorage throw QuotaExceededError when storage is full and SecurityError when storage is disabled (some private browsing modes, enterprise policies). The system must not crash when a secondary storage mechanism is unavailable.


On every page load where the UID already exists, re-set the cookie with a fresh Max-Age. This resets the expiration clock.

This is particularly important for Safari ITP, where client-set cookies expire after 7 days. If a user visits the site every 5 days, each visit refreshes the 7-day window, maintaining the identity indefinitely. Without refresh, a user who visits on day 6 would find their cookie intact, but a user who visits on day 8 would get a new identity.

For server-set cookies, the same principle applies with the 400-day window. A user who visits once per year would lose their identity; a user who visits within 400 days maintains it.

The refresh happens regardless of which storage mechanism the UID was recovered from. Even if the UID was recovered from localStorage, the cookie is re-set with a full 400-day expiry.


All tabs within the same browser profile share cookies and localStorage, but sessionStorage is per-tab. This means:

  • The persistent UID is identical across all tabs (shared via cookies and localStorage).
  • Each tab has its own session ID (separate sessionStorage instances).

This is correct behavior. Each tab is a distinct user session, even though the user is the same person. Do not attempt to synchronize session IDs across tabs.

Cookies are shared across subdomains if the Domain attribute is set (e.g., Domain=.example.com makes the cookie available on shop.example.com, blog.example.com, etc.). localStorage is NOT shared across subdomains — shop.example.com and blog.example.com have separate localStorage instances.

For multi-subdomain sites, the server-set cookie is the primary cross-subdomain identity mechanism. localStorage serves as a per-subdomain backup only.

If your site uses both www.example.com and example.com, set the cookie Domain to .example.com to cover both. Omitting the Domain attribute locks the cookie to the exact hostname that set it.

When your site is embedded in an iframe on a third-party domain, the browser treats your code as a third-party context. Storage is partitioned under CHIPS (Chrome) or blocked entirely (Safari, Brave, Firefox). The UID from the parent page is not accessible, and any UID set in the iframe is isolated to that embedding context.

This is by design. Iframes from other domains should not access the host page’s identity, and the host page should not access the iframe’s identity. If you need to pass identity into an iframe you control, use postMessage with explicit origin validation (see 08-data-handling.md).

The UID is approximately 50 bytes. The full cookie with attributes is well under 200 bytes. This is within the 4KB per-cookie limit enforced by all browsers. Do not store attribution data, consent state, or session metadata in the UID cookie. Use separate cookies or localStorage for additional data to avoid approaching the limit.

Single-page applications that perform client-side navigation may trigger identity resolution multiple times before the first server response arrives. The resolution function must be idempotent: if a UID already exists in any storage mechanism, use it rather than generating a new one. Never generate a new UID if one is already present anywhere in the hierarchy.

Users can clear cookies, localStorage, or all site data at any time. Extensions like Privacy Badger, uBlock Origin, and Cookie AutoDelete may clear storage automatically. The multi-mechanism approach means a user must clear ALL storage types simultaneously to lose their identity. Most clearing actions target cookies only, leaving localStorage intact as a recovery source.


PropertyCookie (server-set)Cookie (JS-set)localStoragesessionStorage
Max lifetime (Chrome/Firefox/Edge)400 days400 daysPermanentTab close
Max lifetime (Safari)400 days (same-IP)7 days7 days without interactionTab close
Max lifetime (Brave)180 days7 daysPermanentTab close
Accessible by JavaScriptIf not HttpOnlyYesYesYes
Sent with HTTP requestsYesYesNoNo
Cross-subdomainIf Domain attribute setIf Domain attribute setNoNo
Cross-tabYesYesYesNo
Size limit4KB per cookie4KB per cookie5-10MB5-10MB
Survives cookie clearingNoNoYesYes (until tab close)
Set byServer (HTTP header)Client (JavaScript)Client (JavaScript)Client (JavaScript)
Private browsingSession onlySession onlySession only (or disabled)Session only

The full per-browser behavioral breakdown, including Firefox ETP strict mode, Edge balanced mode, and private browsing variations, is documented in 05-browser-landscape.md.


Identity resolution sets fields in the page_view payload that downstream systems use to detect identity events:

  • identity.is_new: true — A brand-new UID was generated. No existing identity found in any storage. Equivalent to what a separate identity_init event would convey.

  • identity.resolution_method: "localstorage_recovery" or "sessionstorage_recovery" — UID recovered from backup storage after the primary cookie was lost. Equivalent to what a separate identity_recover event would convey. A high recovery rate indicates cookies are being cleared or expiring.

Both fields are included in every page_view payload. The endpoint can filter on these fields to create derived event streams if needed.

See 04-endpoint-schema.md for the full field reference and 07-consent-integration.md for consent constraints on when payloads are sent.