Skip to content
codefastlabs

Command Palette

Search for a command to run...

@codefast/trackingv0.7.1· Specification

spec-data-subject-rights — DSR Obligations for the Pseudonymous Id

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.

Status: design target for commercial scope, largely met today. The system owns the load-bearing primitives — the consent gate, clear() + server expiry, a per-destination onErasure hook invoked on withdrawal (tracker.erase), and the GA4 delete delegation (buildGa4UserDeletionRequest / submitGa4UserDeletion). This document specifies the minimal-but-sufficient data-subject-rights (DSR) posture around them, and — importantly — argues from the sources against over-building. Access dates 2026-07-18; the "is it personal data" question is fact-dependent and flagged UNCERTAIN — needs legal review.

1. The identifier is pseudonymous, not anonymous

Treat the "anonymous id" (spec-identity) as a pseudonymous device identifier — personal data / a unique identifier — and design as if DSR obligations attach.

  • GDPR: Recital 30 names cookie identifiers as online identifiers; Art. 4(1) lists an online identifier among what makes a person identifiable; Art. 4(5) + Recital 26 hold that pseudonymised data (attributable to a person only with additional information) remains personal data (EDPB Guidelines 01/2025 on Pseudonymisation). A standalone random UUID in the system's own store is genuinely borderline under the Recital 26 "reasonable means" test — UNCERTAIN — but once forwarded to a destination (GA4, Meta) that joins it to a graph, it is personal data at the system boundary, not borderline.
  • CCPA/CPRA: Cal. Civ. Code § 1798.140 "unique identifier" expressly enumerates cookies, persistent identifiers, and probabilistic identifiers, and "personal information" reaches data linkable to a device or household. "No directly-identifying data attached" is therefore not a CCPA exemption.

Consequence for wording: do not call the id "anonymous" in privacy-policy-facing language — call it a pseudonymous device identifier. (The internal anonymousId field name is a precedent-locked API contract; this is about controller-facing documentation, not the symbol.)

2. Rights → responsibility matrix

Actors: System = this library (near-stateless; forwards to destinations, owns only the id cookie + consent record). Controller = the consuming operator (owns the privacy policy, fields DSARs, holds destination admin access). Destination = GA4, Meta, etc.

Right (GDPR / CCPA) Primary responsible party What the SYSTEM MUST provide
Access / Know (Art. 15 / §1798.110) Controller, executing at Destination Expose the id (and destination-side ids, e.g. GA4 client_id from _ga) to the controller's DSAR handler.
Erasure / Delete (Art. 17 / §1798.105) Split: System + Controller (a) clear() — expire the id cookie + drop cache; (b) a per-destination erasure hook; (c) expose the id for destination-keyed deletion.
Opt-out of Sale/Share (§1798.120 / GPC) System enforces; Controller configures The consent gate — closing it stops forwarding, which is the technical enforcement of opt-out.
Withdraw consent (Art. 7(3)) System Already core: withdrawal → clear() + server expiry + destination cookie clear (spec-consent §9, spec-identity §5).
Rectification (Art. 16) Not applicable Nothing — a random UUID has no factual content to correct; document as N/A.
Portability (Art. 20) Controller, at Destination Nothing beyond id exposure — the portable data lives in the destination.

Synthesis: the system owns exactly two DSR-relevant primitives — the gate (opt-out/withdraw enforcement) and clear() + a per-destination erasure hook (erasure) — plus id exposure so the controller can drive access/deletion at the destination. Everything else is the controller's or the destination's job.

3. Destination delegation

Platforms ship their own DSR tooling; the system MUST delegate rather than reimplement.

  • GA4 — a real per-visitor deletion API exists. GA4's client_id (in the _ga cookie) is the per-device analog of an analytics id. The legacy v3 userDeletionRequests:upsert was sunset with Universal Analytics; the current path is the Analytics Admin API properties.submitUserDeletion (developers.google.com/analytics/devguides/config/userdeletion/migration), which keys the subject with a flat clientId (a oneof with userId / appInstanceId / userProvidedData) and needs the analytics.edit OAuth scope. Limits the spec MUST state honestly: removed from the Individual User report within ~72 hours, purged at the next ~bimonthly run, and it does not reach previously-aggregated reports or BigQuery exports.
  • Meta — no per-browser-UUID deletion API. Meta's Data Deletion Request Callback is Facebook-Login-specific (an app-scoped user_id in a signed_request); it does not delete pixel/Conversions-API browser data. The system's realistic lever is stop-send (gate) + clear Meta cookies (spec-destinations §5). The spec MUST NOT imply a Meta per-visitor deletion hook exists.

Posture: the destination interface gains a generic onErasure(id) capability; a GA4 reference binding calls the Admin API submitUserDeletion with the destination client_id; the Meta reference behavior is cookie-clear + stop-send, documented as "no per-visitor deletion API upstream." Do not build a bespoke deletion store.

4. Retention and rotation

There is no hard legal number — it is proportionality (GDPR Art. 5(1)(e) storage limitation, 5(1)(c) minimisation).

  • T1 (SHOULD) — the 1-year cookie Max-Age (spec-identity §2) is a defensible default, not a legal ceiling; document it as a controller-configurable proportionality choice. Destinations impose their own retention (GA4 property retention 2–14 months) the controller MUST align.
  • T2 (SHOULD, optional) — id rotation aids minimisation but is not legally mandated, and it is in tension with the server ITP re-issue (spec-identity §4), which deliberately prolongs one id to beat Safari's cap. Offer rotation as an optional posture the controller may enable, never a default that would fight the ITP-survival design.
  • T3 (MUST) — withdrawal MUST expire the id (Max-Age 0) — already specified; this discharges the identifier-erasure obligation at the system layer.
  • T4 (MUST) — the system sets no retention for destination-held data; that is the destination's/controller's configuration.

5. Minimal-but-sufficient posture

The system stores almost nothing (id cookie + consent record) and forwards to destinations that each ship their own DSR tooling or their own limits. The correct posture — consistent with the project's no-speculative-features rule — is: delegate to destinations + provide a withdrawal/clear primitive + expose the id lifecycle for the controller's privacy policy and DSAR handler. Do not build a DSR request portal, a subject-data store, or a rectification/portability path the system has no data to serve.

The spec asserts only:

  • (a) the id is a pseudonymous device identifier (personal data / unique identifier) — stated plainly for the controller's privacy notice;
  • (b) the system's DSR surface is exactly the gate, clear() + server expiry, a per-destination erasure hook, and id exposure;
  • (c) access, portability, and destination-data deletion are delegated to destinations (GA4 reference binding calls the Admin API submitUserDeletion; Meta = stop-send + cookie-clear);
  • (d) rectification and portability are not applicable at the system layer — say so, do not build them;
  • (e) retention is a controller proportionality choice (1-year default configurable; rotation optional and noted in tension with the ITP re-issue).

Conformance vectors

DSR-V1 — withdrawal clears id. An analytics = false decision → cookie Max-Age 0 + cache dropped + one server-expiry request; no further events send. (Extends spec-identity §5.)

DSR-V2 — withdrawal triggers destination erasure hook. On withdrawal, each registered destination's onErasure(id) is invoked exactly once with the resolved destination identifier, and swallows failures (tracking never breaks the app).

DSR-V3 — GA4 delete binding. Given a client_id and GA4 propertyId, buildGa4UserDeletionRequest emits an Admin API submitUserDeletion request — POST .../v1alpha/properties/{propertyId}:submitUserDeletion with body { "clientId": "<client_id>" } — snapshot the request shape (no network). The legacy v3 { id: { type: "CLIENT_ID", userId } } shape is retired.

DSR-V4 — Meta erasure is cookie-clear + stop-send only. The Meta destination onErasure clears Meta cookies and halts sending, and does not fabricate a non-existent per-visitor deletion API call.

DSR-V5 — id exposure without minting. A read-only accessor returns the current id (for the controller's DSAR handler) without minting one when the gate is closed (composes with the lazy-mint rule, spec-identity §3).

DSR-V6 — GPC → opt-out-of-share. A GPC signal present → ads = false, ad destinations receive no events; the analytics gate is independent.

The standalone-UUID-in-the-system's-own-store personal-data status is UNCERTAIN; the Meta "no per-visitor deletion API for pixel/CAPI" point should be re-verified against Meta docs at finalization (platform DSR tooling changes); GA4 deletion timelines are Google-operational and may shift.

License

Released under the MIT License.