Core Resources
View as Markdown

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
string
Opaque event id (CUID). Used as the cursor for pagination — pass the last id back as ?cursor= to fetch the next page. Stable for the lifetime of the audit row; never reused.
event
enum
FIELD_CHANGEDVERSION_PUBLISHEDSHARED_WITH_YOUACCESS_GRANTEDCOMMENTREPLYMENTIONMIGRATION_AUDIENCE_CHANGEDROPLET_ARCHIVED_BY_SPACEDROPLET_PDF_DOWNLOADED
Event name from the audit registry. 042 added the first four (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.
actor
object· nullable
The User who triggered the event ({ id, name, email }). null for system-emitted events (cascade archives, scheduled cleanups). Renderers fall back to "system" when null.
app
object· nullable
{ 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.
space
object· nullable
{ 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.
details
object
Event-specific payload. Shape depends on 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.
createdAt
timestamp
ISO 8601 timestamp when the audit row was written (server clock). Used as the since cursor when paginating forward in time; results are ordered strictly by createdAt DESC then id DESC for ties.
THE CHANGE EVENT OBJECT
{
  "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.

GET

List change events for a subject

/api/changes

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

subject
string· query· required
app:<id> or space:<id> — id can be a cuid or slug.
since
string· query
Only return events with createdAt > this ISO timestamp.
cursor
string· query
Opaque cursor (event id) from previous response.
limit
number· query
Page size (default 50, max 100).
GET/api/changes
drop changes list --subject app:q2-retro-board --since 2026-06-07T00:00:00.000Z
Response200
{
  "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
}
GET

List change events on everything I watch

/api/changes/me

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

since
string· query
Only return events with createdAt > this ISO timestamp.
cursor
string· query
Opaque cursor (event id) from previous response.
limit
number· query
Page size (default 50, max 100).
GET/api/changes/me
drop changes me --since 2026-06-07T00:00:00.000Z
Response200
{
  "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
}