Event types
Canonical sync event types and their payload shapes.
This is the canonical list of sync event types and their payload shapes. It
finalizes the "exact sync event schema" open item from Todo.md (needed by
Phase 8). The source of truth is packages/protocol/src/events/event-types.ts;
the generated schemas/event.schema.json is the machine-readable equivalent.
The canonical event envelope
{
"event_id": "uuid",
"origin_id": "node_abc",
"origin_sequence": 1042,
"type": "FILE_CREATED",
"payload": { "...": "..." },
"timestamp": "2026-09-02T12:00:00Z"
}| Field | Type | Notes |
|---|---|---|
event_id | string (uuid) | Globally unique; used for idempotent application (§8 dedupe) |
origin_id | string | The node/device that created the event |
origin_sequence | integer ≥ 0 | Monotonically increasing, scoped to the origin. Together with origin_id this provides total ordering per origin and enables cursor-based sync (§18). |
type | enum | One of the canonical types below |
payload | object | Validated against the schema for type |
timestamp | ISO 8601 string | When the event was created at the origin |
Ordering guarantees
- Within one origin:
origin_sequenceis strictly increasing. Events from a single origin are strictly ordered. - Across origins: there is no global total order. Each side tracks a cursor
per origin (
{ origin_id, sequence }) and computes the diff (§18). Event application is idempotent byevent_id, so out-of-order or duplicated delivery across origins cannot create duplicate state.
Canonical event_type values
FILE_CREATED
A file was created.
| Field | Type | Required | Notes |
|---|---|---|---|
file_id | string | yes | Logical file identifier |
parent_folder_id | string | null | no | Omitted for root-level files |
encrypted_name | string | no | Encrypted filename (opaque to the relay) |
content_hash | string | no | BLAKE3 of the latest version |
FILE_DELETED
A file was deleted (represented with a tombstone, §17).
Same payload shape as FILE_CREATED.
FILE_VERSION_ADDED
A new immutable version was added to an existing file.
| Field | Type | Required | Notes |
|---|---|---|---|
file_id | string | yes | Logical file identifier |
version_number | integer ≥ 1 | yes | Monotonic version for this file |
shard_count | integer ≥ 1 | yes | Total shard count for this version |
version_hash | string | yes | BLAKE3 content hash of this version |
parent_folder_id | string | null | no | Omitted for root-level files |
encrypted_name | string | no | Encrypted filename |
content_hash | string | no | Also accepted; redundant with version_hash |
FILE_MODIFIED
A file's content changed. Same payload shape as FILE_VERSION_ADDED (immutable
versions model an edit as a new version).
FILE_SHARD_STORED
A Storage Node committed an encrypted shard to its object store after receiving
it directly over Path A/B WebRTC (not via the Relay buffer). The Relay projects
this into file_locations as NODE_STORED so the catalog knows the shard is
durably stored and downloads can resolve it; origin_id is the storing node.
| Field | Type | Required | Notes |
|---|---|---|---|
file_id | string | yes | Logical file identifier |
version_number | integer ≥ 1 | yes | Version the shard belongs to |
shard_index | integer ≥ 0 | yes | 0-based shard position |
hash | string | yes | BLAKE3 of the stored ciphertext |
size_bytes | integer | no | Stored ciphertext length |
DEVICE_REVOKED
A device lost access (revocation removes only its key envelope, per ADR-0001 — file keys are not rotated).
| Field | Type | Required | Notes |
|---|---|---|---|
device_id | string | yes | The revoked device |
revoked_at | ISO 8601 string | yes | When the revocation happened |
TOMBSTONE_CREATED
Records that an entity was deleted, preventing resurrection by a long-offline device (§17). Retention is governed by the GC policy (§29a).
| Field | Type | Required | Notes |
|---|---|---|---|
entity_type | enum | yes | file or folder |
entity_id | string | yes | The deleted entity's ID |
deleted_at | ISO 8601 string | yes | When the deletion happened |
TOMBSTONE_REMOVED
Restore: removes a tombstone so the entity is live again. Emitted by a device from the Tombstone view; both the Relay and the Storage Node delete their tombstone row, and retained node data is no longer purged at the original retention deadline.
| Field | Type | Required | Notes |
|---|---|---|---|
entity_type | enum | yes | file or folder |
entity_id | string | yes | The restored entity's ID |
FOLDER_CREATED
A folder was created.
| Field | Type | Required | Notes |
|---|---|---|---|
folder_id | string | yes | Folder identifier |
parent_folder_id | string | null | no | Omitted for root-level folders |
encrypted_name | string | no | Encrypted folder name |
FOLDER_DELETED
A folder was deleted.
Same payload shape as FOLDER_CREATED; folder_id is the only required field.
Projection: since Phase 14 F1 both folder events are projected by the Relay (
folderstable) and the Rust node, and are accepted from authenticated devices. AFOLDER_CREATEDwhose id already has a tombstone is acknowledged but not projected, so a long-offline device cannot resurrect a deleted folder. Folders are also carried in snapshots via the"folder"chunk record type.
KEY_ENVELOPE_ADDED
A device published an opaque FEK envelope so a recipient can decrypt a file (§25, Phase 14 F2). The payload is:
| Field | Type | Required | Notes |
|---|---|---|---|
file_id | string | yes | The file the FEK belongs to |
recipient_id | string | yes | device_id or node_id the FEK is sealed for |
recipient_kind | "device" | "node" | yes | Which identity space recipient_id is in |
encrypted_key | string | yes | Opaque client encoding of the sealed FEK |
The Relay projects this into key_envelopes(file_id, recipient_id, recipient_kind, encrypted_key) and never sees the FEK. Revoking a device deletes its
envelopes. Envelopes are carried in snapshots ("key_envelope" chunk record
type), so a full Relay rebuild preserves them; the node stores them opaquely
without being able to open them.
ACTIVITY_LOGGED
A client records a terminal activity outcome so the Activity feed is durable on
both the Relay and the Storage Node and reads the same online or offline. The
Relay journals it in sync_events and serves it from GET /activities; the
Node stores it and serves it over the LAN (GET /nodus/activities).
Privacy: the payload carries no file name (names are E2E; the Relay/Node must
never see them). file_id lets each client resolve the display name from its own
decrypted catalog/tombstone; detail is limited to non-sensitive text.
| Field | Type | Required | Notes |
|---|---|---|---|
activity_id | string | yes | Client-generated uuid; the feed's dedupe key |
kind | enum | yes | upload, download, delete, restore, purge, rename, move, conflict |
outcome | enum | yes | complete or failed |
file_id | string | null | no | File the action concerned |
path | string | null | no | Transfer path (local/relay/buffered/queued/offline) |
detail | string | null | no | Non-sensitive summary or error text |
created_at | ISO 8601 string | yes | When the action finished |
Adding a new event type
- Add a literal to
EventTypesinsrc/events/event-types.ts. - Add it to
EventTypeSchema(the zod enum). - Define a payload schema and register it in
EventPayloadMap. - Regenerate
schemas/and update this document. - Mirror in Rust/Go per the cross-language sync rules in
schema-versioning.md.