Skip to content

Event Redaction

Redaction removes the meaningful content of a previously appended event when you need to erase data — for example, to comply with a right-to-erasure request. Rather than deleting the event entirely, Chronicle replaces the event’s payload with a redaction marker that preserves the sequence number and limited audit metadata. The original event type, occurrence, correlation ID, caused-by identities, and causation types and times remain; original causation property values, revision history, and content hashes do not.

Events are immutable, and their sequence numbers are permanent identifiers. Observers, projections, and reducers may have already processed an event and advanced their positions based on those sequence numbers. Removing the slot entirely would corrupt every downstream position. Redaction instead replaces the content in-place, changes the event type to the internal EventRedacted type, and stores the reason and the original context alongside it.

This means:

  • Observers that replay the event see only the redaction marker, not the original data.
  • Positions and sequence numbers remain stable and consistent.
  • An auditor can still determine that an event existed at that position and who requested its removal.

Redact an event when:

  • A regulation or legal request (e.g. GDPR right to erasure) requires that personal data be removed.
  • An event was appended in error and contains sensitive information that must not be retained.
  • You need to wipe all events associated with a specific event source (e.g. a user account deletion).

Do not use redaction as a way to undo domain decisions. If a correction is being made in the domain — for example, a cancellation, reversal, or override — model that as a new event instead.

To redact one specific event by its sequence number, call Redact on the event sequence and provide a reason:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
public class RedactionUnknownReasonService(IEventLog eventLog)
{
public Task Redact(EventSequenceNumber sequenceNumber) =>
eventLog.Redact(sequenceNumber, RedactionReason.Unknown);
}

View C# snippet source on GitHub

Always provide a meaningful reason when possible:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
public class RedactionWithReasonService(IEventLog eventLog)
{
public Task Redact(EventSequenceNumber sequenceNumber) =>
eventLog.Redact(sequenceNumber, new RedactionReason("GDPR erasure request"));
}

View C# snippet source on GitHub

To redact every event associated with a particular event source — for example, to erase all data for a specific user — pass the event source identifier and a reason:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
public class RedactionByEventSourceService(IEventLog eventLog)
{
public Task RedactAccount(EventSourceId eventSourceId) =>
eventLog.Redact(eventSourceId, new RedactionReason("Account deletion requested"));
}

View C# snippet source on GitHub

If you only want to redact a specific type of event rather than all events for the event source, provide the event types:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
[EventType]
public record RedactionPersonalDetailsRecorded(string Name, string SocialSecurityNumber);
[EventType]
public record RedactionAddressChanged(string Street, string City);
public class RedactionByEventSourceAndTypesService(IEventLog eventLog)
{
public Task RedactPersonalData(EventSourceId eventSourceId) =>
eventLog.Redact(
eventSourceId,
new RedactionReason("PII erasure"),
typeof(RedactionPersonalDetailsRecorded),
typeof(RedactionAddressChanged));
}

View C# snippet source on GitHub

Redaction is modelled as a system event in the system event sequence. When you call Redact, Chronicle appends an EventRedactionRequested (for a single event) or an EventsRedactedForEventSource (for an event source) system event to the internal system sequence. A built-in reactor observes these system events and performs the actual in-place replacement in the target event sequence.

The replacement stores an EventRedacted content record inside the event document that captures:

  • The reason for the redaction.
  • The original event type, so auditors know what kind of event was removed.
  • The original Occurred, CorrelationId, and CausedBy identity chain.
  • The original Causation entries’ types and occurrence times, without their property values.

The event’s own context fields reflect the redaction action. The original payload, revision history, and content hashes are removed from event sequence storage. Redaction cannot reach copies made before redaction, such as database oplogs or backups, or events already forwarded to other event stores through outbox/inbox. Handle those copies separately.

Observing redacted events in Reactors and Reducers

Section titled “Observing redacted events in Reactors and Reducers”

When an event is redacted, the EventRedacted event is dispatched to any observer that was subscribed to the original event type and also includes EventRedacted in its subscription. This lets Reactors and Reducers react to the removal of data — for example, to clean up read models or trigger downstream notifications.

  • Read model cleanup: If a Reducer builds state from an event that is later redacted, subscribing to EventRedacted lets you remove or anonymise that state in reaction.
  • Audit and notifications: A Reactor can notify external systems or write a compliance log entry whenever sensitive data is erased.
  • Cross-aggregate side effects: If redacting one event should trigger cancellations or clean-up in other parts of the system, a Reactor can coordinate that work.

Add a handler method that accepts EventRedacted alongside the event type(s) you already handle. The Kernel will only dispatch EventRedacted to your Reactor when the redacted event belongs to a type that your Reactor is also subscribed to.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Reactors;
[EventType]
public record RedactionPersonRegistered(string Name);
public class RedactionPersonReactor : IReactor
{
public Task Registered(RedactionPersonRegistered @event, EventContext context)
{
// Handle the original event
return Task.CompletedTask;
}
public Task Redacted(EventRedacted @event, EventContext context)
{
// Called only when a RedactionPersonRegistered event (a type this reactor handles) is redacted.
// @event.OriginalEventType is typeof(RedactionPersonRegistered).
// Use this to undo any side effects produced by the original event.
return Task.CompletedTask;
}
}

View C# snippet source on GitHub

The EventRedacted record is defined in Cratis.Chronicle.Events and carries:

PropertyDescription
ReasonThe RedactionReason provided at the time of redaction.
OriginalEventTypeThe CLR Type of the event that was redacted.
OccurredWhen the original event occurred.
CorrelationIdThe correlation identifier of the original event.
CausationThe original causation types and occurrence times, without property values.
CausedByThe identity chain that caused the original event.

The same approach applies to Reducers. Include EventRedacted handling inside the IReducerFor<TReadModel> implementation to update the read model when a source event is erased.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Reducers;
[EventType]
public record RedactionReducerPersonRegistered(string Name);
public record RedactionPersonReadModel(string Name);
public class RedactionPersonReadModelReducer : IReducerFor<RedactionPersonReadModel>
{
public RedactionPersonReadModel Registered(RedactionReducerPersonRegistered @event, RedactionPersonReadModel? current, EventContext context) =>
(current ?? new RedactionPersonReadModel(string.Empty)) with
{
Name = @event.Name
};
public RedactionPersonReadModel? Redacted(EventRedacted @event, RedactionPersonReadModel? current, EventContext context) =>
// Return null to remove the read model, or return a sanitised version
null;
}

View C# snippet source on GitHub

The EventRedacted event is delivered to an observer only if the observer was already subscribed to the original event type. An observer that subscribes to [PersonRegistered, EventRedacted] will not receive an EventRedacted notification when an unrelated event type (e.g. OrderPlaced) is redacted.

The Workbench lets you redact events without writing any code:

  1. Open the Event Log view for the event store and namespace you want to manage.
  2. Select the event you want to redact.
  3. Click the Redact button in the event detail panel.
  4. In the dialog that appears, enter the reason for the redaction.
  5. Click OK to confirm.

Confirming records the redaction request; a built-in reactor then replaces the event’s content with the redaction marker, usually within moments. The event slot remains visible in the sequence but no longer contains the original data. Before you report the data as removed, reopen the event and check that its content has been replaced.