Change Tracking
Design rationale for grant-relative incremental sync via changes_since cursors, not canonical changelog streams.
Date: 2026-07-07 (revised from 2026-04-06)
Scope: Design rationale and decision history for grant-relative incremental sync. The normative mechanics live in spec-core: Section 4 defines stream semantics, the snapshot model, and tombstones; Section 8 defines the changes_since query surface.
Decision
Change tracking in PDPP uses grant-relative incremental sync, not canonical changelog streams.
There are no {stream}_changes streams in the protocol. Change tracking is a property of the resource server query API: a client passes a changes_since cursor and receives only records that changed within its grant-authorized field projection since that cursor.
This design was chosen over a canonical CDC-style changelog after analysis of prior art (Microsoft Graph delta queries, OData change tracking, Google Calendar incremental sync). The key finding: a canonical stored changelog cannot be made privacy-safe without becoming grant-relative anyway. Two clients with overlapping grants (one authorized for fields A+B, another only for A) cannot share a single changelog without leaking metadata about field B to the A-only client. Grant-relative delta queries solve this cleanly.
The consequence that shaped the normative text: eligibility for a changes_since response must be computed on the grant-authorized projection, not the full record. Selecting rows on full-record changes and projecting afterward leaks that hidden fields changed. spec-core Section 8 states this as a conformance requirement.
Normative surface (pointers, not restatement)
- Stream semantics (
append_only,mutable_state), internal version history, the snapshot model, and the tombstone envelope: spec-core Section 4. - The
changes_since/next_changes_sincetoken space, its separation fromcursor/next_cursor, session-horizon anchoring across pages, projection-safe eligibility, and cursor expiry (HTTP 410cursor_expired, full re-sync): spec-core Section 8.
Why not canonical {stream}_changes streams
The March 2026 draft defined {stream}_changes as a companion stream generated by the resource server. It was rejected for three reasons:
-
Privacy leakage. A canonical changelog stored once for all clients cannot be filtered per-grant without becoming grant-relative anyway. The
changed_fieldslist alone leaks that a field changed, even if the field value is redacted. -
Conceptual confusion.
{stream}_changeslooks like a first-class stream but has different semantics from real streams (it is derived, not collected). Calling it a stream creates a false equivalence. -
Prior art alignment. Microsoft Graph, OData, and Google Calendar all model change tracking as query-relative delta views, not as canonical streams. The pattern is established and well understood.
What this does not cover
-
Point-in-time reconstruction. Reconstructing the full state of a stream at a past timestamp is not supported in v0.1. It requires materializing historical state from version history, which is expensive. Deferred.
-
Field-level change subscriptions. "Notify me when display_name changes" is a push/webhook concern. Deferred.
-
Changes between collections. The resource server can only track changes it has observed. If a value changed and changed back between two collection runs, the net change is zero and no delta is returned. This is an inherent limitation of pull-based collection, not a protocol flaw; resource server implementations should document it.