---
title: Change Event
kind: resource
slug: change
summary: "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."
---

# 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](/docs/resources/droplet) or [Space](/docs/resources/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](/docs/resources/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

### 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` (enum: `FIELD_CHANGED` / `VERSION_PUBLISHED` / `SHARED_WITH_YOU` / `ACCESS_GRANTED` / `COMMENT` / `REPLY` / `MENTION` / `MIGRATION_AUDIENCE_CHANGE` / `DROPLET_ARCHIVED_BY_SPACE` / `DROPLET_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.

### JSON example

```json
{
  "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

### List change events for a subject

`GET /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).

**CLI**

```bash
drop changes list --subject app:q2-retro-board --since 2026-06-07T00:00:00.000Z
```

**MCP**

```json
{
  "tool": "drop_changes_list_for_subject",
  "arguments": {
    "subject": "app:q2-retro-board",
    "since": "2026-06-07T00:00:00.000Z"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/changes' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "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

`GET /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).

**CLI**

```bash
drop changes me --since 2026-06-07T00:00:00.000Z
```

**MCP**

```json
{
  "tool": "drop_changes_list_for_me",
  "arguments": {
    "since": "2026-06-07T00:00:00.000Z"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/changes/me' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "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
}
```
