Skip to content

Subject

The subject of an event is the target the event is about — the identity used to key compliance concerns such as PII encryption. In most systems the subject and the EventSourceId are the same, but they don’t have to be.

Consider a ShippingAddressChanged event. The EventSourceId is the order identifier (that is what binds all events for that order together), but the subject is the customer the address is about. PII encryption keys are held per-subject, not per-event-source, so that every event that touches the same person can be decrypted with the same key regardless of which aggregate produced it.

Order stream — EventSourceId = orderId

subject

ShippingAddressChanged

Subject = customerId

per-subject PII encryption key

When you don’t specify a subject, Chronicle uses the EventSourceId as the subject. This is the right default for most slices where the aggregate and the subject coincide:

using Cratis.Chronicle;
using Cratis.Chronicle.Events;
[EventType]
public record SubjectAuthorRegistered(string Name);
public class SubjectAuthorService(IEventStore eventStore)
{
public Task Register(EventSourceId authorId, string name) =>
// Subject defaults to authorId; encryption keys for any PII on SubjectAuthorRegistered
// are keyed by authorId.
eventStore.EventLog.Append(authorId, new SubjectAuthorRegistered(name));
}

When the event source is different from the subject, pass Subject explicitly. Subject has an implicit conversion from EventSourceId, so mixing the two is painless:

using Cratis.Chronicle;
using Cratis.Chronicle.Events;
[EventType]
public record SubjectShippingAddressChanged(string Street);
public class SubjectShippingService(IEventStore eventStore)
{
public Task ChangeAddress(EventSourceId orderId, Subject customerId, string street) =>
eventStore.EventLog.Append(
eventSourceId: orderId,
@event: new SubjectShippingAddressChanged(street),
subject: customerId);
}

Subject also converts implicitly from string and Guid. Kotlin, Java, and TypeScript don’t currently expose a subject append option at all — only C# and Elixir do.

Rather than threading subject through every call site, decorate the relevant property on the event type with [Subject]. Chronicle will read the property value automatically when no explicit subject is provided:

using Cratis.Chronicle;
using Cratis.Chronicle.Compliance.GDPR;
using Cratis.Chronicle.Events;
public record SubjectOrderId(Guid Value);
public record SubjectCustomerId(Guid Value);
[EventType]
public record SubjectShippingAddressChangedWithImplicitSubject(
SubjectOrderId Order,
[Subject] SubjectCustomerId Customer,
[PII] string Street,
[PII] string City);
public class SubjectImplicitSubjectService(IEventStore eventStore)
{
// Subject is derived from the Customer property — no explicit subject needed.
public Task ChangeAddress(SubjectOrderId orderId, SubjectCustomerId customerId, string street, string city) =>
eventStore.EventLog.Append(
orderId.Value,
new SubjectShippingAddressChangedWithImplicitSubject(orderId, customerId, street, city));
}

An explicit subject argument always takes precedence over a [Subject] property. This declarative form is C#-only — Elixir requires passing subject: explicitly on every append (shown above); Kotlin, Java, and TypeScript don’t support a subject at all yet.

When an event type declares [PII] properties, Chronicle encrypts those properties at append time using a per-subject encryption key. The key is created on first use and stored in the encryption key store. Because the key lives under the subject, every event that shares a subject is encrypted with the same key — and once that key is deleted (for example, to honor a right-to-erasure request), every PII property across every event for that subject becomes unrecoverable in one operation.

Read more about PII compliance.