Published in openvibe-contracts v0.33.0 (docs/adr/ADR-026-event-redaction.md), rendered as is.

ADR-026: Producer redaction of its own events (payload.redacts)

Status: Accepted 2026-09-23; implemented in OpenVibe.Events (server/redaction.js, server/store.js, test/redaction.test.js) and used by OpenVibe.Chat (chat.message.deleted).

Context and current evidence

OpenVibe.Events keeps every event for its retention period and serves it again through pull, GET /api/v1/events/:id, SSE replay, queued deliveries and DLQ replays (ADR-004, ADR-005). A public chat.message.created carries the message text and the author. When the message was deleted in Chat, the stored event stayed replayable, anonymously, for 30 days. Deleting a row in the producer did not reach the copy in Events, and no consumer could tell that the content was withdrawn.

Decision

Public realtime replay window

Browsers (signed out or signed in) are replayed public events received in the last REALTIME_PUBLIC_REPLAY_SECONDS only (default 300), which is enough to ride out a reconnect. An older cursor gets event: gap with reason: "public_window" (from_seq, to_seq, latest_seq, window_seconds), so the SSE stream cannot be used to page through a month of public history. subject events addressed to the viewer, and service viewers, keep the whole retention. Redacted events are replayed as their tombstones.

Event payload contracts

Each event type's payload has its own contract, named after the event type. The major version is the envelope version. The file is contracts/events/payloads/<event_type>.v<version>.json, so a consumer validates contracts.validate(${env.event_type}@${env.version}, env.payload) after checking for a tombstone. The owner is the producing service, and the service manifest lists the type in eventsProduced. Contracts with catalog status planned describe events that the owner has not emitted yet (tools.job.* in v0.30.0).

Alternatives considered

Migration consequences

Chat publishes chat.message.deleted with redacts for every delete path. The operator backfill redacted what Chat had deleted earlier. Other producers adopt the directive on their own *.deleted or *.removed events when those carry personal content.

Rollback

A producer stops sending redacts. Tombstones already written stay tombstones, because the original payload is gone by design.

Acceptance tests

OpenVibe.Events test/redaction.test.js: every read path (pull, GET, anonymous/signed-in/service SSE replay, queued delivery, DLQ replay) returns the tombstone at an unchanged seq; another source's event is refused with 403 and the whole batch is not stored; a malformed directive is 422; a directive event is never redacted. The public window test checks that anonymous replay older than 300 s yields gap public_window. Contracts: events.redaction-directive@1 and events.tombstone-payload@1 fixtures; every event payload contract has valid and invalid fixtures.