The log remembers: PII, crypto-shredding, and erasure in Chronicle
Sooner or later, every system that stores personal data receives a request that reads something like: erase everything you have about this person. Regulations such as the GDPR turn that request from a courtesy into an obligation with a deadline. And if your system is event-sourced, the request collides head-on with the property you chose the architecture for in the first place: the log is append-only. Events are written once and never altered — that is what makes them trustworthy as history.
This post is about how Chronicle, our open-source event-sourcing database, resolves that collision. The short version: the log keeps remembering that things happened, while the personal content of those things stops existing. No rewrites, no deleted sequence numbers, no broken projections.
Why the obvious answers fail
Section titled “Why the obvious answers fail”The first instinct is to delete the events. That breaks the system in ways that are worse than the problem being solved. Every projection, reactor, and read model in a Chronicle event store tracks its position in the sequence by event number; removing a slot mid-sequence corrupts every one of those positions. Replays and audits lose the fact that something happened at all. And an event log that can be edited on demand is no longer an honest record of anything.
The second instinct is to encrypt the personal values — and stop there. Encryption does protect the values, but a single application-wide key makes erasure impossible to scope: destroy the key and you have shredded everyone’s data, not the one person who asked for it. Keep the key and you have kept the ability to read the data, which is precisely what the erasure request said must end.
Both approaches fail for the same underlying reason: they treat personal data as an application concern that the store is unaware of. Chronicle takes the opposite approach — the store knows which values are personal, and it tracks whose they are.
Marking personal data where it is declared
Section titled “Marking personal data where it is declared”The first half of the mechanism is an adornment. Marking a value [PII] — a C# attribute, equally a Kotlin/Java annotation, TypeScript decorator, or Elixir macro — tells Chronicle that the value holds personally identifiable information. When an event carrying that value is appended, the kernel encrypts the value before it reaches storage. When a projection or observer later reads the event, the value is decrypted transparently. Application code sees plaintext; the event log holds ciphertext.
You can mark a single event property. Here it is, the same mechanism, in every client Chronicle ships:
using Cratis.Chronicle.Compliance.GDPR;using Cratis.Chronicle.Events;
[EventType]public record PiiAttrEmployeeRegistered( [PII] string FirstName, [PII] string LastName, string Department);import io.cratis.chronicle.compliance.Piiimport io.cratis.chronicle.events.EventType
@EventTypedata class PiiAttrEmployeeRegistered( @Pii val firstName: String, @Pii val lastName: String, val department: String)import io.cratis.chronicle.compliance.Pii;import io.cratis.chronicle.events.EventType;
@EventTyperecord PiiAttrEmployeeRegistered( @Pii String firstName, @Pii String lastName, String department) {}import { eventType, pii } from '@cratis/chronicle';
@eventType()class PiiAttrEmployeeRegistered { @pii() firstName = ''; @pii() lastName = ''; department = '';}defmodule MyApp.Events.PiiAttrEmployeeRegistered do use Chronicle.Events.EventType, id: "pii-attr-employee-registered"
defstruct [:first_name, :last_name, :department]
pii(:first_name) pii(:last_name)endBut the approach Chronicle steers you toward is marking the type. Most personal data in a well-modeled domain already lives in concept types — SocialSecurityNumber, DateOfBirth, EmailAddress — rather than bare strings. Mark the concept once — here for a person’s name:
using Cratis.Chronicle.Compliance.GDPR;
[PII]public record PiiAttrConceptPersonName(string Value) : ConceptAs<string>(Value){ public static readonly PiiAttrConceptPersonName NotSet = new(string.Empty); public static implicit operator string(PiiAttrConceptPersonName name) => name.Value; public static implicit operator PiiAttrConceptPersonName(string value) => new(value);}import io.cratis.chronicle.compliance.Piiimport io.cratis.chronicle.concepts.ConceptAs
@Piidata class PiiAttrConceptPersonName(override val value: String) : ConceptAs<String>import io.cratis.chronicle.compliance.Pii;import io.cratis.chronicle.concepts.ConceptAs;
@Piirecord PiiAttrConceptPersonName(String value) implements ConceptAs<String> { @Override public String getValue() { return value; }}import { pii } from '@cratis/chronicle';import { ConceptAs } from '@cratis/fundamentals';
@pii()class PiiAttrConceptPersonName extends ConceptAs<string> { constructor(value: string) { super(value); }}defmodule MyApp.Compliance.Pii.PersonName do use Chronicle.Concept, type: :string pii()endand every property of that type, in every event and every read model, is protected from then on. The classification travels with the type into value objects, nested structures, collections — the marker lands on the individual leaf values wherever they end up, and the document keeps its shape. That is what makes this cross-cutting in practice rather than in theory: protection stops being a thing each developer must remember per event, because using the type and protecting the data are the same decision. A new event written months later cannot forget the attribute, because there is no attribute to forget.
The marker also accepts a details note — a legal basis, a retention rule — stored with the event schema so that classification decisions remain visible to compliance tooling and audits rather than living in someone’s head.
Erasure as a key lifecycle, not a deletion
Section titled “Erasure as a key lifecycle, not a deletion”The second half of the mechanism is how data disappears. Every [PII] value is encrypted under a key that belongs to the subject the data is about — one key per person, scoped to a namespace and looked up by the event’s event source identifier. Personal values for one person are unreadable without that person’s key and unaffected by anyone else’s.
That gives erasure a precise, surgical shape. The right-to-erasure request becomes one call — deleting the subject’s key:
await eventStore.PII.DeleteEncryptionKeyFor(subjectId);After that, the events for that person are all still there: same sequence numbers, same positions, every non-personal field intact. Only the [PII] properties now release as empty, because the key that could decrypt them no longer exists. This is crypto-shredding — the data is not deleted so much as it becomes permanently unreadable, mathematically, while the structure of the log survives untouched. Projections keep their positions. Audits keep the fact that events occurred. The person’s data is gone.
Making that erasure stick is the genuinely hard part, and it is where most homegrown crypto-shredding schemes quietly fail. A deleted key has a way of coming back: a new append provisions a fresh key for the same subject, an event-store subscription copies the key into another store, a cache hands back a stale entry. Chronicle treats each of these as a first-class problem: the erasure records a fence in every key store of the namespace — keyed by the destroyed key’s fingerprint — and after that the store refuses to provision a key for the subject, refuses to accept the destroyed key material back at any revision, and refuses to copy it in from elsewhere. If the person later returns under a lawful basis, a new key can be authorized explicitly — and it cannot decrypt anything written before the erasure.
For the rarer case where the event content itself must go — the whole payload, not just marked values — Chronicle has a second mechanism: event redaction. A redacted event keeps its sequence number and audit context but its content is replaced by a marker recording what type of event it was, why it was redacted, and when. Downstream positions stay valid, and observers can react to the redaction itself — cleaning read models, notifying downstream systems.
What this does not do
Section titled “What this does not do”A mechanism this convenient deserves an honest list of limits, and Chronicle’s own documentation is blunt about them.
Erasure is scoped to a namespace — the tenancy boundary. The same person in two namespaces has two keys, and erasing in one deliberately leaves the other alone; multi-tenant systems issue one erasure per namespace the person appears in. Event source identifiers themselves cannot be encrypted — they are the lookup keys for the encryption keys — so a sensitive identity should be stored as a marked property, with a non-sensitive surrogate as the identifier. And after an erasure, appending new [PII] data for that subject fails loudly rather than silently restarting protection; if the person legitimately comes back, authorizing a new key is a deliberate act that Chronicle logs — without recording who authorized it, because the store does not know. Your erasure procedure still owns the paper trail.
The client experience also varies by language: the C# client validates the most (refusing, for instance, to let you mark an event source identifier as PII), while some other clients accept markers without the same checks today — the documentation spells out exactly which paths each client covers.
Most importantly: none of this is a compliance certification, and we don’t present it as one. What Chronicle provides are mechanisms — classification at the type level, per-subject encryption, fenced erasure, auditable redaction — that map the regulatory demands onto operations a database can actually perform. Whether a given erasure satisfies a given obligation, and what your procedure must document alongside it, remains a judgment for you and your advisers. That is not a disclaimer tacked on at the end; it is the division of labor the whole design rests on. The store owns the mechanics of forgetting. You own the decision to forget.
Trying it
Section titled “Trying it”The compliance documentation walks through marking data, read-model interactions, the key lifecycle, and what a single erasure reaches. If you are new to Chronicle, the .NET quickstart on this blog gets a store running locally in minutes from the official templates — mark a property [PII] on one of its events and inspect the stored event in Chronicle’s bundled Workbench to see the mechanism with your own eyes. Everything described here is shipped in Chronicle 18.1 and MIT licensed, like the rest of the Cratis stack.