Change Event
Drop's audit trail rendered as a queryable change stream. Every typed-field change, version publish, sharing-mode change, and access grant gets a row — keyed by subject (a Droplet or Space) and ordered newest-first.
A Change Event is the structured record of one mutation on a Drop subject: who did it (actor — or null for system events), what subject it happened on (a Droplet or Space), the event name from the audit registry (FIELD_CHANGED, VERSION_PUBLISHED, SHARED_WITH_YOU, ACCESS_GRANTED, plus the pre-042 comment / mention / archive events), and an event-specific details payload. The audit log IS the change-event store — there is no parallel table.
The read APIs split into per-subject (listForSubject — every event on one Droplet or Space) and per-viewer (listForMe — every event across the viewer's Watch set, minus muted rows). Both are cursor-paginated, share the same response envelope ({ events, nextCursor, hasMore }), and apply read-time coalescing: runs of typed-field changes by the same actor inside a 5-minute window collapse into one row (details.coalesced = true, with the per-field detail in details.changes).
SC-04 access invariant — read-time access enforcement is non-negotiable. listForSubject requires the viewer to have read access to the subject (non-readers get 404, mirroring the existence-hiding pattern). listForMe filters events through the same access check at read time, so losing access to a Droplet mid-watch silently removes its events from the stream. The same access gate is applied at fan-out before any Notification row is written — never trust the read filter alone. Drafts are excluded from the stream entirely (the draft access chokepoint at lib/drafts.ts is upstream of audit emission).
The Change Event object
Every Change Event carries these attributes.
Attributes
id back as ?cursor= to fetch the next page. Stable for the lifetime of the audit row; never reused.FIELD_CHANGED, VERSION_PUBLISHED, SHARED_WITH_YOU, ACCESS_GRANTED); the rest are pre-042 events that ride the same stream. Renderers branch on this discriminator to choose the bell-row template and the icon.{ id, name, email }). null for system-emitted events (cascade archives, scheduled cleanups). Renderers fall back to "system" when null.{ id, slug, name } of the Droplet the event is scoped to. Set when the subject is a Droplet; null when the subject is a Space-level event. Mutually exclusive with space.{ id, slug, name } of the Space the event is scoped to. Set for Space-level events (membership change, audience change, Space-archive cascade). Mutually exclusive with app.event: FIELD_CHANGED carries { fieldKey, fieldLabel, from, to }; VERSION_PUBLISHED carries { version, fileSize, notes? }; SHARED_WITH_YOU / ACCESS_GRANTED carry { actorEmail, role? }. Coalesced rows carry { coalesced: true, count, windowStartedAt, changes: [...] } — read-time coalescing collapses runs of typed-field changes by the same actor inside a 5-minute window. The bell-row renderer discriminates on the event name + details.coalesced flag.since cursor when paginating forward in time; results are ordered strictly by createdAt DESC then id DESC for ties.{
"id": "evt_clx7",
"event": "FIELD_CHANGED",
"actor": {
"id": "usr_alex",
"name": "Alex Chen",
"email": "alex@himarley.com"
},
"app": {
"id": "app_q2",
"slug": "q2-retro-board",
"name": "Q2 Retro Board"
},
"space": null,
"details": {
"fieldKey": "status",
"fieldLabel": "Status",
"from": "in-review",
"to": "decided"
},
"createdAt": "2026-06-07T14:00:00.000Z"
}Methods
Endpoints to operate on a Change Event. The example switches between curl, drop CLI, and MCP via the dropdown — your choice persists across the page.
List change events for a subject
Return the change-event stream for one subject (Droplet or Space). Newest first, cursor-paginated. Read-time coalescing is applied — runs of typed-field changes by the same actor in a 5-min window collapse into a single coalesced row (per lib/change-events/coalesce.ts). The caller must have read access to the subject — non-readers get 404 (existence-hiding pattern). On 4xx: INVALID_SUBJECT, INVALID_SINCE, INVALID_CURSOR, SUBJECT_NOT_FOUND.
Parameters
app:<id> or space:<id> — id can be a cuid or slug.drop changes list --subject app:q2-retro-board --since 2026-06-07T00:00:00.000Z{
"events": [
{
"id": "evt_clx7",
"event": "TYPE_FIELD_VALUE_CHANGED",
"actor": {
"id": "usr_alex",
"name": "Alex Chen",
"email": "alex@himarley.com"
},
"app": {
"id": "app_q2",
"slug": "q2-retro-board",
"name": "Q2 Retro Board"
},
"space": null,
"details": {
"fieldKey": "status",
"fieldLabel": "Status",
"from": "in-review",
"to": "decided"
},
"createdAt": "2026-06-07T14:00:00.000Z"
}
],
"nextCursor": null,
"hasMore": false
}List change events on everything I watch
Return the change-event stream across every subject the viewer watches (implicit + explicit, minus muted). Same shape as listForSubject. Powers the User Agent "what's new everywhere" view (v2 — see data-model.md § UserSpaceVisit forward-compat note). Mute always wins — MUTED watch rows suppress fan-out and read-API inclusion both. On 4xx: INVALID_SINCE, INVALID_CURSOR.
Parameters
drop changes me --since 2026-06-07T00:00:00.000Z{
"events": [
{
"id": "evt_clx8",
"event": "VERSION_PUBLISHED",
"actor": {
"id": "usr_kai",
"name": "Kai Patel",
"email": "kai@himarley.com"
},
"app": {
"id": "app_q2",
"slug": "q2-retro-board",
"name": "Q2 Retro Board"
},
"space": null,
"details": {
"version": 3,
"fileSize": 41928,
"notes": "Fixed typo"
},
"createdAt": "2026-06-07T15:30:00.000Z"
}
],
"nextCursor": null,
"hasMore": false
}