Identity Management
Edition 3.1.1
Consent dependency: Everything in this section describes the full technical capability. Every persistence write and every recovery call is gated by the base persistence predicate defined below, which depends on the consent state in Consent Integration.
User Identifier Format
Section titled “User Identifier Format”The User Identifier (UID) is the token that recognizes a returning browser. It has two parts: a random UUID (a standard 128-bit random identifier) and the Unix timestamp (seconds) of issuance, joined by a dot.
{uuid_v4}.{unix_timestamp_seconds}Example:
f81d4fae-7dec-4ec9-a765-00a0c91e6bf6.1647291600The UUID component is a real version-4 UUID (version nibble 4, variant nibble 8, 9, a, or b). The timestamp suffix makes the relationship’s age readable without a lookup.
The sole canonical, executable UID validation pattern is defined once in the Endpoint Schema contracts. This section references it and deliberately carries no competing pattern. Validators are built from that single definition, never from examples.
Server-issued only. The server creates every lasting UID, generated server-side with a cryptographically secure random generator by the cookie endpoint defined in section 04. The browser never creates one. If the server does not provide one, nothing persistent is created.
Opacity is not anonymity. The UID embeds no IP address, device fingerprint, or PII. It is nevertheless linkable pseudonymous personal data: the key to whatever the holder can join to it. It requires the same transport, access-control, log-redaction, and retention controls as any identifier. It is not an authenticator and grants nothing by possession.
The Base Persistence Predicate
Section titled “The Base Persistence Predicate”Three rules control the endpoint’s Set-Cookie/create behavior and whether the browser may write or keep the stored UID/recovery pair (uiaf_uid and uiaf_recovery). UIAF calls these rules the base persistence predicate. All three conditions must hold:
- Consent lifecycle is
resolved(ornot_applicableunder an explicitly established T0 policy), - the effective
analytics_storagepermission isallowed, and - the deployment has documented its first-party analytics / persistent-identity purpose.
A recovery call also requires the deployment’s explicit continuity recovery policy (see Recovery Policy below). That policy gates recovery calls only. It is not a second browser-retention predicate: under the privacy policy, a validly retained identity pair may remain in storage while no recovery call ever occurs.
Known withdrawal, deletion, suppression, credential expiry/revocation, corruption, or loss of the base predicate forbids recovery and atomically purges the pair from every permitted script-storage class before any persistence write or send. Every disallowed endpoint state returns the contract’s observable denial without setting identity.
Cookie Authority
Section titled “Cookie Authority”A valid server-set uiaf_uid cookie is authoritative. On every resolution, if a valid cookie exists, it wins over any divergent in-memory or script-storage state.
The client never writes the persistent uiaf_uid cookie. No document.cookie write, no Cookie Store API write, no equivalent path exists for it: not when the cookie is divergent, and not even when it is absent.
- Absent cookie: server issuance or recovery. The client routes through the coordinated
/api/uiaf/cookiecreate call (which carries no UID; the server mints one), or a credential- and policy-authorized recover call (which carries onlyuiaf_recovery, never a UID). The server alone issuesSet-Cookie. - Refresh is middleware-only. Only the server renews the cookie, on responses it handles. The browser never refreshes it.
- Mandatory cookie re-read. The client re-reads the actual cookie the browser holds at three points: after any cookie-endpoint response, before any script-storage persistence write, and before every send. The endpoint’s response body is a candidate for comparison and diagnostics only; it never seeds storage, payloads, or sends as canonical. If the cookie is absent or unreadable, identity remains indeterminate (no persistence writes, no canonical adoption) until same-origin server projection or later server-injected canonical state resolves it.
Cookie Endpoint — Client-Facing Contract
Section titled “Cookie Endpoint — Client-Facing Contract”The normative /api/uiaf/cookie contract lives in the Endpoint Schema auxiliary contracts. What the client can rely on:
- Closed request surface. The only inbound UID surface is the validated
uiaf_uidcookie. A create request carries no UID; a recover request carries onlyuiaf_recovery. Any UID anywhere in the request body, including a canonical, previously issued, or credential-accompanied one, returns400with noSet-Cookie. - Canonical UID validation (syntax, length, control characters, per the section-04 pattern) applies to the inbound cookie before the value reaches any header, template, or log line.
Content-Type: application/jsonis required; form-encoded posts are rejected.Originis authoritative: an invalid or cross-siteOriginrejects; a validOriginwith absentSec-Fetch-Siteaccepts; a validOriginwith cross-site Fetch Metadata rejects. Metadata absence is never proof of same-origin.- No-overwrite: any request carrying a valid inbound cookie, matching or divergent, receives the observable no-set response, never a replacement.
- Consent gating evaluates the authoritative server-side consent source (CMP cookie or equivalent, read per request). A client-posted consent assertion is never consent proof. Every disallowed state returns the observable denial without setting identity.
- Rate limiting per the contract; an invalid inbound
uiaf_uidcookie is expired and treated as absent; raw UIDs never appear in request, response, error, or rate-limit logs.
Cookie Configuration
Section titled “Cookie Configuration”| Attribute | Value | Why |
|---|---|---|
| Name | uiaf_uid | Consistent, recognizable, collision-unlikely |
| Secure | true | HTTPS only |
| SameSite | Lax | Blocks cross-site subrequests; allows top-level navigation |
| HttpOnly | false | JavaScript needs read access for resolution, the mandatory re-reads, and script-storage sync. Writes never occur from script regardless |
| Max-Age | 34560000 | The draft RFC 6265bis request ceiling (~400 days). This is what the server requests — not a lifetime any browser guarantees |
| Path | / | Site-wide |
| Domain | site-specific | Explicit (e.g. .example.com) for subdomain sharing; omit for single-host sites |
SameSite, cookie Path, CORS, and first-party naming are not authentication or isolation boundaries. Native, same-origin serving improves resilience but is never unblockable and never guarantees a fixed lifetime.
Recovery Credential (uiaf_recovery)
Section titled “Recovery Credential (uiaf_recovery)”Restoring a previously server-issued UID requires the opaque recovery credential uiaf_recovery. “Opaque” means the value carries no readable content; only the server can check it.
- Storage classes: localStorage/sessionStorage only, never a cookie, and only the same selected script-storage classes as its paired
uiaf_uid. The pair is written together and purged atomically: a partial or quota-failed write removes both rather than leaving an orphan. - The credential is bounded, non-semantic browser input (base64url, 22–128 characters, ≥128 bits of CSPRNG entropy; the closed shapes live in the section-04 route contract), server-bound to the UID, deployment/tenant, issuance version, expiry, and recovery policy. Stateful opaque handles or authenticated tokens are implementation choices, but expiry, revocation, binding, and rotation behavior are mandatory.
- Returned only over TLS with
Cache-Control: no-store; never logged raw; never an authenticator for data-subject requests. - Syntax validation alone is never proof that a candidate UID was minted by this deployment. A stored UID without its verifiable credential is not recoverable: it is treated as absent, and resolution proceeds to server issuance.
Script-Storage Synchronization
Section titled “Script-Storage Synchronization”syncScriptStorage mirrors the identity into browser storage. The rule is simple: the UID and its recovery credential are saved together, or not at all. If any write fails, both are removed. The mirror never writes a cookie, and with no credential nothing is mirrored at all: the cookie remains authoritative in memory, and script storage simply holds nothing.
// PSEUDOCODE — script storage only; there is NO cookie write path here
function syncScriptStorage(uid, recovery): // Base predicate re-checked immediately before every persistence write. // Predicate LOSS is not a no-op: per the base contract, it atomically // purges the retained pair from every selected script-storage class // before any persistence write or send. if not persistencePredicateHolds(): purgeIdentityPairFromSelectedClasses() return // zero write, zero send — and no // already-retained pair survives in // any selected class if recovery is null: // No credential -> no orphan script UID. The cookie stays // authoritative in memory; nothing is persisted to script storage. purgeIdentityPairFromSelectedClasses() return // Only the deployment-SELECTED classes participate. An unselected or // deliberately unavailable class is not a failure and never converts a // valid single-class configuration into an accidental purge. try: for storageClass in selectedScriptStorageClasses(): // LS, SS, or both writePairAtomically(storageClass, {uiaf_uid: uid, uiaf_recovery: recovery}) // All-selected-classes atomicity: reaching here means every selected // class holds the complete pair. catch (error): // Partial/quota/security failure in ANY selected class: purge BOTH // members from EVERY selected class — never an orphan anywhere. purgeIdentityPairFromSelectedClasses()// Fixtures: LS-only config; SS-only config; both-classes config;// second-class-failure rollback (first class written, second fails ->// both classes end empty).// Predicate-loss purge (negative): pair pre-exists in LS-only, SS-only,// and both-selected configurations; the predicate flips false immediately// before sync -> both keys are absent from ALL selected classes afterward,// with zero writes and zero sends; unselected classes remain outside this// function's mutation scope per the selected-class contract.Identity Resolution on Page Load
Section titled “Identity Resolution on Page Load”- Server-side
resolveIdentity(request)reads and validates the inbound cookie (invalid means absent) and only reads. Creation and refresh belong to the endpoint and middleware. - Client-side
resolveIdentity()reads the cookie, then script storage, and requests create/recover as above. It returns identity only,{uid, is_new, resolution_method, confidence}, withis_newalwaysfalsewhenuidisnull. It never manufactures or returns session ownership:session_idand sequence state come solely from the separately awaited session allocator above, acquired after authorization and before any sequence allocation. The function emits no events; thesessionpayload’s identity fields carry everything the endpoint needs.
The resolution function is idempotent within a document, and no path in it mints a persistent UID client-side.
Session State (uiaf_session_state)
Section titled “Session State (uiaf_session_state)”A session is a single tab’s visit, distinct from the persistent identity that survives across visits. Session state lives in exactly one registered sessionStorage key, uiaf_session_state, holding the exact record:
{"session_id": "<uuid-v4>", "next_seq": 0}Each session numbers its events in order, starting at 0, and the first event of a session is numbered session_seq: 0 and carries session_start: true. The number is allocated only after the event is authorized and immediately before the payload is frozen, so a withheld event never burns a sequence value.
Legacy keys. The retired pre-3.0 session keys (uiaf_session, uiaf_session_id) are members of the registry’s closed legacy_purge_only set in uiaf-storage-keys.json, the sole source for legacy purge names: purged on sight, never read, migrated, or written as live v3 state. This section maintains no competing list.
One Allocator per Session
Section titled “One Allocator per Session”Duplicating a browser tab clones sessionStorage. Without a guard, two tabs would number events for the same session_id and collide. The one-allocator protocol prevents that:
- The sole lock name is
uiaf-session-alloc:<session_id>; the sole advisory channel isuiaf-session-claim. - Where Web Locks exists, a new document requests the origin-wide exclusive lock without
ifAvailable, with a 250 ms acquisition deadline implemented via anAbortSignal. An ordinary same-tab navigation has a bounded opportunity to hand off after the outgoing document releases its lifetime lock. Acquisition permits continuation; the document holds the lock for its active lifetime. Timeout, abort, or a live competing owner causes conservative rekeying. The specification does not promise a slow ordinary navigation can never rekey. - Rekey: the document overwrites only its cloned
uiaf_session_statewith a fresh UUID-v4 session andnext_seq: 0, then must acquire the corresponding fresh lock before allocating. If that fresh lock cannot be acquired within 250 ms, it retries once with another UUID and otherwise emits withsession_id: nulland no sequence. - Without Web Locks, every new document unconditionally rekeys before its first authorized event, honestly degrading to per-document sessions. A CSPRNG-created distinct ID is the ownership proof.
uiaf-session-claimannouncements may accelerate rekeying, but silence never proves exclusive ownership and cannot preserve an inherited session. - Page lifecycle code releases ownership on
pagehide(before navigation/BFCache suspension) and onpageshowwithpersisted: truereacquires the lock or conservatively rekeys. Where these APIs are absent, the document falls back to the conservative path. - The endpoint’s (
session_id,session_seq) conflict rule is a safety net, never the primary allocator. No second live session-storage representation exists.
Sessionization policy: a UIAF session is tab-scoped with no inactivity bound; endpoints wanting 30-minute-style sessions re-sessionize downstream.
Storage Registry
Section titled “Storage Registry”The machine-readable storage-key registry is owned by the core protocol and published at docs-site/public/schema/v3/uiaf-storage-keys.json (with the enum/event registries beside it). This section renders that registry and adds nothing to it. The registered artifacts relevant to identity:
| Artifact | Class | Notes |
|---|---|---|
uiaf_uid | Server-set cookie (authoritative) + localStorage/sessionStorage sync under the base predicate | Zero client cookie writes |
uiaf_recovery | localStorage/sessionStorage only — same classes as its paired uiaf_uid, purged atomically with it | Never a cookie |
uiaf_session_state | sessionStorage, exact {session_id, next_seq} record | Sole session representation |
uiaf_attribution | localStorage + sessionStorage (Attribution Capture) | No backup cookie exists |
uiaf_retry_queue | localStorage (delivery contract, section 04) | Purged on downgrade |
uiaf_dirty_baseline | sessionStorage — the canonical projection string (never a hash) | Contract below |
uiaf-session-alloc:<session_id>, uiaf-session-claim, uiaf-identity-issuance, uiaf-consent-wakeup | Runtime IPC — not storage | Sole source: the closed runtime_ipc section of uiaf-enums.json (web_lock_name_template, broadcast_channel = uiaf-session-claim, identity_issuance_web_lock = uiaf-identity-issuance, consent_wakeup_broadcast_channel = uiaf-consent-wakeup; no aliases). The retry-queue drain lock uiaf-retry-drain (retry_drain_web_lock) is likewise registered in that section and rendered in the Endpoint Schema. uiaf-consent-wakeup is wake-only: a message carries no consent state and no UID/identity state, and its receipt only triggers a re-read of the authoritative consent sources. Values are mirrored from that sole source — no competing list is maintained here |
_uiaf_token is an auxiliary transient URL parameter documented in the section-04 handoff contract; it is never stored and is not a registry row. The registry’s legacy_purge_only and absent_by_design sets are rendered exclusively from uiaf-storage-keys.json; this section enumerates neither.
Dirty Baseline (uiaf_dirty_baseline)
Section titled “Dirty Baseline (uiaf_dirty_baseline)”To avoid resending unchanged state, the client remembers the last state it sent or durably queued as one canonical string (the UTF-8 RFC 8785 (JCS) serialization of a fixed projection tree) in uiaf_dirty_baseline. On each evaluation it rebuilds that string and compares it byte-for-byte: same bytes, no send. conversion and identify are explicit calls and bypass this gate.
Multi-Tab Convergence and Consent Propagation
Section titled “Multi-Tab Convergence and Consent Propagation”Convergence across tabs is eventual, never atomic:
- A valid server cookie wins on every resolution, in every tab.
- With no cookie, issuance runs inside a separate, short-lived, origin-wide Web Lock with the exact name
uiaf-identity-issuance(sole source:runtime_ipc.identity_issuance_web_lockinuiaf-enums.json, short-lived UID issuance/recovery coordination), neveruiaf-session-alloc:<session_id>, and never reused from the lifetime session-allocator lock. Inside that boundary the holder re-reads the cookie, the identity/recovery credential state, and the current consent and policy, and only the winning coordinator calls the endpoint. Where Web Locks is unavailable or acquisition fails, best-effort wake-up/re-read coordination applies and convergence stays eventual, never atomic. - Concurrent no-cookie requests may each receive a
Set-Cookie; response ordering wins. The browser-held cookie after the final race and re-read is authoritative, and every tab adopts it via the mandatory re-reads andstorageevents. - Consent wake-up broadcasting uses the exact channel
uiaf-consent-wakeup. The registered binding isruntime_ipc.consent_wakeup_broadcast_channel=uiaf-consent-wakeupinuiaf-enums.json, with no alias, distinct from the session-claim channelruntime_ipc.broadcast_channel=uiaf-session-claim. The channel is wake-only runtime IPC, not storage: a message carries no consent state and no UID or identity state, says only “consent may have changed,” is never trusted as consent proof, and its receipt triggers only a re-read of the authoritative CMP/GPC/server-side consent state. Consent (CMP/GPC/server-authoritative state) is re-read immediately before capture, persistence writes, retry queueing, retry drain, and every network send, and on initialization, focus/visibility return, andpageshow/BFCache restore. Where BroadcastChannel is unavailable, those re-read points are the fallback; no localStorage epoch key exists. If capability decreased, pending payloads are purged, rebuilt, or dropped synchronously before any transmission. The server re-reads its authoritative consent state on every relevant request; browser IPC never updates server state.
Recovery Policy and User-Initiated Clearing
Section titled “Recovery Policy and User-Initiated Clearing”Known withdrawal is authoritative. CMP/GPC withdrawal, a site-provided deletion action, or a received deletion/suppression state purges identity before resolution. Recovery never runs from another UIAF store afterward, and a stale tab may not resurrect or transmit an old UID (enforced by the send-time re-reads).
The platform limitation, stated plainly: cookie expiry, manual cookie-only deletion, and some extension clearing are indistinguishable through browser APIs. This specification does not claim to infer user intent from partial storage loss.
Deployment policy for ambiguous partial loss, explicitly configured:
privacy(the default): partial loss is treated as possible intent. No recovery; fresh server issuance.continuity(explicit opt-in): credential-verified recovery is permitted under the base predicate.
Known withdrawal overrides both. The analytics-vs-privacy tradeoff of each choice must be documented by the deployment. A full browser “clear site data” clears every UIAF store; a new persistent identity is created only when the current effective permissions allow.
Edge Cases
Section titled “Edge Cases”Multiple Tabs
Section titled “Multiple Tabs”Cookies and localStorage are shared across tabs; sessionStorage is per-tab; in-memory state is not shared. “The UID is identical across tabs” is true of the stores, not of each tab’s memory until convergence completes. Session state follows the one-allocator protocol above; a duplicated tab never continues the original session_id.
Subdomain Navigation
Section titled “Subdomain Navigation”Cookies are shared across subdomains when Domain is set (e.g. .example.com); localStorage is per-origin and is not. For multi-subdomain sites the server-set cookie is the cross-subdomain identity mechanism; script storage is a per-origin recovery aid only.
iframe Embedding
Section titled “iframe Embedding”In a third-party embedding context, storage is partitioned under CHIPS where supported (Chrome/Edge 114+; Firefox 141 re-enabled it; Safari 18.4 shipped it, Safari 18.5 temporarily removed it, and Safari 26.2 restored support) and otherwise blocked or partitioned by default (Safari/Brave block; Firefox partitions via Total Cookie Protection). Brave has no Storage Access API path: requestStorageAccess is always rejected, so embedded contexts on Brave get neither third-party cookies nor a permission-based escape hatch. Embedded identity is isolated by design; to pass identity into an iframe you control, use postMessage with explicit origin validation. Never accept a message without verifying event.origin against an allowlist.
Browser Storage Clearing
Section titled “Browser Storage Clearing”Handled by the Recovery Policy section above: the framework does not treat surviving stores as license to resurrect identity. The configured policy and the known-withdrawal rule decide.
Storage Mechanism Behavior
Section titled “Storage Mechanism Behavior”| Property | Cookie (server-set) | localStorage | sessionStorage |
|---|---|---|---|
| Safari lifetime | ITP does not cap an aligned server-set cookie; Safari’s maximum is undocumented (the draft RFC 6265bis request ceiling is 400 days). Server-set cookies are generally more resilient — not immune, not guaranteed | Purged with all script-writable storage after 7 days of Safari use without recognized user interaction (tap/click/keyboard as first party — passive or full-page navigation does not reset the clock; per WebKit commit a2db53cd97, 2022-06-08) | Tab lifetime |
| Chrome/Firefox/Edge lifetime | Up to the requested Max-Age (subject to browser policy) | No time-based expiry | Tab lifetime |
| Brave lifetime | Both JS and HTTP cookies capped at 6 months (v1.83+) | Persistent | Tab lifetime |
| Decorated-navigation rule (Safari) | Cookies set via document.cookie on a page whose landing URL carries any query string or fragment and whose referrer is an ITP-classified tracker are capped to 24 hours — ITP does not inspect cookie contents, and named click IDs are industry examples of decoration, not WebKit’s predicate. (UIAF sets no script cookies, so this class never contains the identity) | n/a | n/a |
| CNAME/IP cloaking (Safari) | Third-party-ish serving is detected via WebKit’s prefix heuristic (first half of the address differs: /16 for IPv4, /64 for IPv6, per current WebKit documentation) — “same IP” shorthand is not the rule | n/a | n/a |
| Size limits | RFC 6265 floor minimums: ≥4,096 bytes/cookie, ≥50/domain, ≥3,000 total — floors, not uniform ceilings | 5 MiB per origin | 5 MiB per origin |
| Sent with HTTP requests | Yes | No | No |
| Cross-subdomain | With Domain attribute | No | No |
The full per-browser landscape, with dated primary sources, is in Browser Landscape. ITP 2.3 (2019-09-23) is the last numbered ITP release, a historical fact that does not imply tracking-prevention behavior stopped evolving.
Identity Signals in the Payload
Section titled “Identity Signals in the Payload”Identity resolution sets fields in the session payload:
identity.is_new: true: a brand-new UID was server-issued on this load. Alwaysfalsewhenuidisnull: ephemeral identity is not an initialization event.identity.resolution_method: exactly one of the shared-schema enum valuescookie,localstorage_recovery,sessionstorage_recovery,new,ephemeral(definitions in the Endpoint Schema); a high recovery rate measures storage-policy impact.identity.session_startandidentity.session_seq: from the allocator above;session_seqis required wheneversession_idis non-null.
Field definitions and the event registry live in the Endpoint Schema; consent constraints in Consent Integration.
Normative Requirements
Section titled “Normative Requirements”BCP 14 requirement keywords are normative only inside the identified blocks below; all other prose in this section is explanatory.
- UIAF-02-COOKIE-001 — The client MUST NOT write the persistent
uiaf_uidcookie throughdocument.cookie, the Cookie Store API, or any equivalent path, in any state. The server alone issuesSet-Cookie; refresh is middleware-only. - UIAF-02-COOKIE-002 — The client MUST re-read the browser-held cookie after any cookie-endpoint response, before any script-storage persistence write, and before every send; while the cookie is absent or unreadable, identity MUST remain indeterminate with no persistence writes and no canonical adoption.
- UIAF-02-ISSUE-001 — Where Web Locks is available and acquisition succeeds, UID issuance and recovery coordination MUST run inside the origin-wide Web Lock named
uiaf-identity-issuance, and the holder MUST re-read the cookie, the recovery-credential state, and the current consent and policy inside the boundary, with only the winning coordinator calling the endpoint. Where Web Locks is unavailable or acquisition fails, the client MUST use the specified best-effort wake-up/re-read coordination path, with convergence remaining eventual, never atomic — absence of Web Locks is not nonconforming. In all cases the session-allocator lockuiaf-session-alloc:<session_id>MUST NOT be used or reused for issuance. - UIAF-02-PAIR-001 — Script persistence MUST write a valid UID and its paired
uiaf_recoveryatomically in the same selected storage classes, rolling both back on any failure. A script-storage UID without its credential MUST NOT be created or retained. - UIAF-02-CONSENT-001 — Authoritative consent MUST be re-read immediately before capture, every persistence write, retry queueing, retry drain, and every send, and on initialization, focus/visibility return, and
pageshowwithpersisted: true. When capability has decreased, pending payloads MUST be purged, rebuilt, or dropped synchronously before any transmission.