PII Attribute
The [PII] attribute marks data as personally identifiable information (PII) under GDPR. When Chronicle sees this attribute on an event property or a type, it encrypts the value automatically when the event is written to the event log and decrypts it transparently on read.
Personal data rarely sits in a flat list of properties. A date of birth arrives wrapped in a VerifiedDateOfBirth value object alongside who verified it; a diagnosis carries the condition and the clinician together. Chronicle follows the marker down through that structure: whether you mark a concept nested inside a value object, or the value object type itself, encryption lands on the individual values at the bottom. The document keeps its shape, each value stays independently encrypted, and the release on read mirrors the encryption on write.
using Cratis.Chronicle.Compliance.GDPR;Attribute definition
Section titled “Attribute definition”The signature, for reference:
// C#[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter)]public sealed class PIIAttribute(string details = "") : Attribute
// TypeScriptexport function pii(details?: string): PropertyDecorator & ClassDecoratorThe optional details parameter lets you record why the value is classified as PII — for example, the legal basis under which it is collected or the retention period. This information is stored in the event schema and can be used by compliance reporting tools.
using Cratis.Chronicle.Compliance.GDPR;
[PII("Collected under GDPR Art. 6(1)(b) — necessary for contract performance")]public record PiiAttrPersonName(string Value) : ConceptAs<string>(Value);import { pii } from '@cratis/chronicle';import { ConceptAs } from '@cratis/fundamentals';
@pii('Collected under GDPR Art. 6(1)(b) — necessary for contract performance')class PiiAttrPersonName extends ConceptAs<string> { constructor(value: string) { super(value); }}Where the attribute is valid
Section titled “Where the attribute is valid”| Target | Supported | Notes |
|---|---|---|
ConceptAs<T> class | Yes | Preferred approach — marks the concept type itself as PII |
| Event property | Yes | Marks a single property of an event as PII |
| Value object class | Yes | Marks every value the type holds as PII, however deeply nested |
| Property typed as a value object | Yes | Same effect, scoped to that one property |
| Collection property | Yes | The collection is encrypted as a whole — see Collections are encrypted as a whole |
EventSourceId or EventSourceId<T> | No | Throws PIINotSupportedOnEventSourceId at runtime |
Applying to an event property
Section titled “Applying to an event property”You can mark a single property on an event record as PII. This is useful when the property type is a primitive and you cannot or do not want to introduce a dedicated concept type.
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
@EventType(id = "pii-attr-employee-registered")data class PiiAttrEmployeeRegistered( @Pii val firstName: String, @Pii val lastName: String, val department: String)Java does not support this workflow yet.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)endimport { eventType, pii } from '@cratis/chronicle';
@eventType()class PiiAttrEmployeeRegistered { @pii() firstName = ''; @pii() lastName = ''; department = '';}When this event is written, FirstName and LastName are encrypted. Department is stored as plaintext.
Applying to a ConceptAs type
Section titled “Applying to a ConceptAs type”The preferred approach is to mark the ConceptAs<T> type itself as PII. Every property across every event that uses this type is then automatically encrypted — you declare the rule once and it applies everywhere.
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 { pii } from '@cratis/chronicle';import { ConceptAs } from '@cratis/fundamentals';
@pii()class PiiAttrConceptPersonName extends ConceptAs<string> { constructor(value: string) { super(value); }}Applying to a value object
Section titled “Applying to a value object”Not every piece of personal data is a single value. When a whole value object is personal — a diagnosis, a passport, a home address — mark the type itself. Every value it holds is then treated as PII wherever that type appears, without annotating each member.
using Cratis.Chronicle.Compliance.GDPR;
// Every value this type holds is personal, so mark the type once.[PII]public record PiiAttrDiagnosis(string Condition, string DiagnosedBy);
// Both Condition and DiagnosedBy are encrypted wherever a PiiAttrDiagnosis appears.public record PiiAttrPatientRecord(string Name, PiiAttrDiagnosis Diagnosis);Chronicle pushes the marker down to the individual values rather than encrypting the object as one blob. In storage you still see a Diagnosis sub-document with a Condition and a DiagnosedBy field; both hold ciphertext. That matters for more than tidiness: the shape is what lets the read model materialize back into its value-object type, and it keeps each value separately encrypted rather than fused into a single opaque string.
The same happens when you mark a property whose type is a value object — the effect is identical, just scoped to that one property instead of to the type everywhere.
Nesting
Section titled “Nesting”The marker does not have to sit at the top level. A ConceptAs<T> marked [PII] is found wherever it ends up — directly on an event, one level down inside a value object, or deeper still.
using Cratis.Chronicle.Compliance.GDPR;
[PII]public record PiiAttrDateOfBirth(string Value) : ConceptAs<string>(Value){ public static implicit operator PiiAttrDateOfBirth(string value) => new(value);}
// The concept sits one level down, inside a value object.public record PiiAttrVerifiedDateOfBirth(PiiAttrDateOfBirth DateOfBirth, string VerifiedBy);
// Chronicle still finds it: dateOfBirth.dateOfBirth is encrypted, verifiedBy is not.public record PiiAttrExpressVerification(string Name, PiiAttrVerifiedDateOfBirth DateOfBirth);Here only dateOfBirth.dateOfBirth is encrypted. verifiedBy sits beside it in the same value object and stays readable, because the concept carries the classification, not its container.
This is what makes concept-level [PII] worth reaching for: you declare the rule once on the type, and it holds no matter how the value is later composed into events and read models.
Constraints
Section titled “Constraints”EventSourceId is not supported
Section titled “EventSourceId is not supported”Applying [PII] to a type that inherits from EventSourceId or EventSourceId<T> throws PIINotSupportedOnEventSourceId at runtime:
using Cratis.Chronicle.Compliance.GDPR;using Cratis.Chronicle.Events;
// ❌ This will throw PIINotSupportedOnEventSourceId[PII]public record PiiAttrEmployeeId(Guid Value) : EventSourceId<Guid>(Value);Event source identifiers are used to look up encryption keys and group events. Encrypting them would make key lookup impossible. If the identifier itself is sensitive, use a non-sensitive surrogate (such as a random Guid) as the event source identifier and store the sensitive value in a [PII]-marked event property.
Collections are encrypted as a whole
Section titled “Collections are encrypted as a whole”A [PII] marker on a collection property behaves differently from one on a value object. The collection is encrypted as a single value rather than element by element, and its shape is restored when it is released. That keeps the data protected, but the individual elements are not separately encrypted and cannot be queried or indexed on the server.
When you need per-element encryption, model the collection as its own person-scoped read model keyed by the subject and join at the query edge, rather than nesting it inside a larger document.
Details parameter
Section titled “Details parameter”The details parameter is a free-text description stored in the event schema. It is never used for encryption — it exists solely to record why a value is classified as PII for compliance documentation and auditing purposes.
using Cratis.Chronicle.Compliance.GDPR;
[PII("Full legal name — required for contract identification")]public record PiiAttrLegalName(string Value) : ConceptAs<string>(Value);import { pii } from '@cratis/chronicle';import { ConceptAs } from '@cratis/fundamentals';
@pii('Full legal name — required for contract identification')class PiiAttrLegalName extends ConceptAs<string> { constructor(value: string) { super(value); }}