Change Tracking
Grant-relative incremental sync via `changes_since` cursors — not canonical changelog streams.
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.
How it works
For append_only streams
Incremental sync is straightforward: new records are added, existing records never change. A client passes a cursor (typically the cursor_field value of the last record it received) and the resource server returns records added since that cursor.
For mutable_state streams
The resource server maintains internal version history. When a client queries with changes_since, the resource server:
- Identifies all records that changed since the cursor within the granted stream.
- Applies the grant's field projection: only fields the client is authorized to see.
- Returns only records where at least one authorized field changed. If only unauthorized fields changed on a record, that record does not appear in the response.
- Includes tombstone entries for records that were deleted since the cursor.
This ensures a client authorized for fields A and B cannot infer that field C changed, even if C was modified.
Query pattern
GET /v1/streams/profile/records?changes_since=<cursor>
Authorization: Bearer <access_token>To start from the beginning, pass the documented sentinel:
GET /v1/streams/profile/records?changes_since=beginning
Authorization: Bearer <access_token>The changes_since cursor is either the literal sentinel beginning for the initial sync or an opaque token from a previous changes response's next_changes_since. It carries the client's last sync position and the grant's field projection context. Clients MUST treat returned cursors as opaque and MUST NOT construct internal version cursors.
If a changes response is paginated, use next_cursor only with the cursor parameter to continue that response. Store next_changes_since as the bookmark for the next sync session.
Tombstones
When a record is deleted, incremental sync responses include a tombstone:
{
"object": "record",
"id": "playlist_123",
"stream": "playlists",
"deleted": true,
"deleted_at": "2026-04-01T10:00:00Z"
}Cursor expiry
Resource servers MAY expire historical version data after a retention period. If a client's changes_since cursor is too old, the server returns HTTP 410 Gone. The client must perform a full re-sync.
Why not canonical {stream}_changes streams
The previous design (March 2026) defined {stream}_changes as a companion stream generated by the resource server. This design was rejected for the following 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 (e.g., "what did the profile look like on March 1?") is not supported in v0.1. This requires the resource server to materialize historical state from version history, which is expensive. Deferred.
-
Field-level change subscriptions: "Notify me when display_name changes." This is a push/webhook concern. Deferred.
-
Changes between collections: The resource server can only track changes it has observed. If a user's display name 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. It should be documented clearly in resource server implementations.
Auth Design
Bearer tokens at both boundaries — wire format and semantics. Identity provider and token issuance are out of scope.
Deferred Concerns
Issues identified during design and review that are intentionally out of scope for v0.1. Each item is named precisely so it can be referenced from the core spec and tracked for future versions.