Working with compliance from the client
Chronicle’s PII encryption is applied transparently by the kernel. From a client perspective, your responsibility is to annotate your domain model correctly so that the kernel knows which values to encrypt. There are two places to put this annotation, and you will typically use both in the same codebase.
Option 1 — Annotate your event types
Section titled “Option 1 — Annotate your event types”Apply [PII] directly to individual properties on your event records. This is the simplest starting point and works well when a value is PII only in the context of one specific event.
using Cratis.Chronicle.Compliance.GDPR;using Cratis.Chronicle.Events;
[EventType]public record ComplianceClientEmployeeRegistered( [PII] string FirstName, [PII] string LastName, string Department, DateTimeOffset StartDate);import io.cratis.chronicle.compliance.Piiimport io.cratis.chronicle.events.EventTypeimport java.time.Instant
@EventTypedata class ComplianceClientEmployeeRegistered( @Pii val firstName: String, @Pii val lastName: String, val department: String, val startDate: Instant)import io.cratis.chronicle.compliance.Pii;import io.cratis.chronicle.events.EventType;
import java.time.Instant;
@EventTyperecord ComplianceClientEmployeeRegistered( @Pii String firstName, @Pii String lastName, String department, Instant startDate) {}defmodule MyApp.Events.ComplianceClientEmployeeRegistered do use Chronicle.Events.EventType, id: "compliance-client-employee-registered"
defstruct [:first_name, :last_name, :department, :start_date]
pii(:first_name) pii(:last_name)endimport { eventType, pii } from '@cratis/chronicle';
@eventType()class ComplianceClientEmployeeRegistered { @pii() firstName = ''; @pii() lastName = ''; department = ''; startDate = new Date();}FirstName and LastName are encrypted when the event is written. Department and StartDate are stored as plaintext.
When to use this approach
Section titled “When to use this approach”Use property-level annotation when:
- The property type is a primitive (
string,int) and you do not need a dedicated concept type. - The value is PII in this event but is conceptually not a PII concept elsewhere.
- You are incrementally adding compliance to an existing model and want to make targeted changes.
Limitation
Section titled “Limitation”The main drawback of property-level annotation is that you must remember to add [PII] every time a new event carries the same kind of value. If a future event includes an employee’s name without the attribute, it will be written as plaintext.
Option 2 — Annotate your ConceptAs types
Section titled “Option 2 — Annotate your ConceptAs types”Apply [PII] to a ConceptAs<T> concept type. Chronicle then encrypts every event property that uses that type, automatically, across all events.
using Cratis.Chronicle.Compliance.GDPR;
[PII]public record ComplianceClientPersonName(string Value) : ConceptAs<string>(Value){ public static readonly ComplianceClientPersonName NotSet = new(string.Empty); public static implicit operator string(ComplianceClientPersonName name) => name.Value; public static implicit operator ComplianceClientPersonName(string value) => new(value);}import io.cratis.chronicle.compliance.Piiimport io.cratis.chronicle.concepts.ConceptAs
@Piidata class ComplianceClientPersonName(override val value: String) : ConceptAs<String>import io.cratis.chronicle.compliance.Pii;import io.cratis.chronicle.concepts.ConceptAs;
@Piirecord ComplianceClientPersonName(String value) implements ConceptAs<String> { @Override public String getValue() { return value; }}defmodule MyApp.Compliance.Client.PersonName do use Chronicle.Concept, type: :string pii()endimport { pii } from '@cratis/chronicle';import { ConceptAs } from '@cratis/fundamentals';
@pii()class ComplianceClientPersonName extends ConceptAs<string> { constructor(value: string) { super(value); }}Any event that uses PersonName is automatically encrypted — no further annotation is needed:
using Cratis.Chronicle.Events;
[EventType]public record ComplianceClientEmployeeRegisteredWithConcept(ComplianceClientPersonName Name, string Department);
[EventType]public record ComplianceClientEmployeeNameChanged(ComplianceClientPersonName NewName); // also encryptedimport io.cratis.chronicle.events.EventType
@EventTypedata class ComplianceClientEmployeeRegisteredWithConcept(val name: ComplianceClientPersonName, val department: String)
// also encrypted@EventTypedata class ComplianceClientEmployeeNameChanged(val newName: ComplianceClientPersonName)import io.cratis.chronicle.events.EventType;
@EventTyperecord ComplianceClientEmployeeRegisteredWithConcept(ComplianceClientPersonName name, String department) {}
// also encrypted@EventTyperecord ComplianceClientEmployeeNameChanged(ComplianceClientPersonName newName) {}defmodule MyApp.Compliance.Client.ConceptUsagePersonName do use Chronicle.Concept, type: :string pii()end
defmodule MyApp.Compliance.Client.EmployeeRegisteredWithConcept do use Chronicle.Events.EventType, id: "compliance-client-employee-registered-with-concept"
defstruct name: %MyApp.Compliance.Client.ConceptUsagePersonName{}, department: ""end
defmodule MyApp.Compliance.Client.EmployeeNameChanged do use Chronicle.Events.EventType, id: "compliance-client-employee-name-changed"
# Also encrypted — reuses the same concept. defstruct new_name: %MyApp.Compliance.Client.ConceptUsagePersonName{}endimport { eventType } from '@cratis/chronicle';
@eventType()class ComplianceClientEmployeeRegisteredWithConcept { constructor(readonly name: ComplianceClientPersonName, readonly department: string) {}}
@eventType()class ComplianceClientEmployeeNameChanged { constructor(readonly newName: ComplianceClientPersonName) {} // also encrypted}When to use this approach
Section titled “When to use this approach”Use concept-level annotation when:
- The value represents a domain concept that is inherently personal (a person’s name, email address, phone number, national ID).
- The same kind of value appears in multiple events and you want consistent protection without repeating the attribute.
- You are building a new domain model and want compliance baked in from the start.
This is the recommended approach for any value that is personal by nature.
Combining both approaches
Section titled “Combining both approaches”The two approaches complement each other. Use concept types for inherently personal domain values, and use property-level annotation for one-off cases where creating a concept type is not warranted.
using Cratis.Chronicle.Compliance.GDPR;using Cratis.Chronicle.Events;
[PII]public record ComplianceClientEmailAddress(string Value) : ConceptAs<string>(Value){ public static readonly ComplianceClientEmailAddress NotSet = new(string.Empty); public static implicit operator string(ComplianceClientEmailAddress email) => email.Value; public static implicit operator ComplianceClientEmailAddress(string value) => new(value);}
[EventType]public record ComplianceClientCustomerRegistered( ComplianceClientPersonName Name, // encrypted via concept type ComplianceClientEmailAddress Email, // encrypted via concept type [PII] string PhoneNumber, // encrypted via property annotation string Country); // plaintextimport io.cratis.chronicle.compliance.Piiimport io.cratis.chronicle.concepts.ConceptAsimport io.cratis.chronicle.events.EventType
@Piidata class ComplianceClientEmailAddress(override val value: String) : ConceptAs<String>
@EventTypedata class ComplianceClientCustomerRegistered( val name: ComplianceClientPersonName, // encrypted via concept type val email: ComplianceClientEmailAddress, // encrypted via concept type @Pii val phoneNumber: String, // encrypted via property annotation val country: String // plaintext)import io.cratis.chronicle.compliance.Pii;import io.cratis.chronicle.concepts.ConceptAs;import io.cratis.chronicle.events.EventType;
@Piirecord ComplianceClientEmailAddress(String value) implements ConceptAs<String> { @Override public String getValue() { return value; }}
@EventTyperecord ComplianceClientCustomerRegistered( ComplianceClientPersonName name, // encrypted via concept type ComplianceClientEmailAddress email, // encrypted via concept type @Pii String phoneNumber, // encrypted via property annotation String country) { // plaintext}defmodule MyApp.Compliance.Client.CombiningPersonName do use Chronicle.Concept, type: :string pii()end
defmodule MyApp.Compliance.Client.CombiningEmailAddress do use Chronicle.Concept, type: :string pii()end
defmodule MyApp.Compliance.Client.CustomerRegistered do use Chronicle.Events.EventType, id: "compliance-client-customer-registered"
defstruct name: %MyApp.Compliance.Client.CombiningPersonName{}, email: %MyApp.Compliance.Client.CombiningEmailAddress{}, phone_number: "", country: ""
# name and email are encrypted via their concept types; phone_number is # encrypted via this property-level annotation; country stays plaintext. pii(:phone_number)endimport { eventType, pii } from '@cratis/chronicle';import { ConceptAs } from '@cratis/fundamentals';
@pii()class ComplianceClientEmailAddress extends ConceptAs<string> { constructor(value: string) { super(value); }}
@eventType()class ComplianceClientCustomerRegistered { name: ComplianceClientPersonName = new ComplianceClientPersonName(''); // encrypted via concept type email: ComplianceClientEmailAddress = new ComplianceClientEmailAddress(''); // encrypted via concept type @pii() phoneNumber = ''; // encrypted via property annotation country = ''; // plaintext}EventSourceId cannot be marked PII
Section titled “EventSourceId cannot be marked PII”Do not apply [PII] to types that inherit from EventSourceId or EventSourceId<T>. Chronicle throws PIINotSupportedOnEventSourceId because event source identifiers are required for key lookup and cannot be encrypted.
using Cratis.Chronicle.Compliance.GDPR;using Cratis.Chronicle.Events;
// ❌ This will throw PIINotSupportedOnEventSourceId at startup[PII]public record ComplianceClientCustomerId(Guid Value) : EventSourceId<Guid>(Value);import io.cratis.chronicle.compliance.Piiimport io.cratis.chronicle.concepts.EventSourceId
// This will throw PiiNotSupportedOnEventSourceId at registration@Piidata class ComplianceClientCustomerId(override val value: String) : EventSourceIdimport io.cratis.chronicle.compliance.Pii;import io.cratis.chronicle.concepts.EventSourceId;
// This will throw PiiNotSupportedOnEventSourceId at registration@Piirecord ComplianceClientCustomerId(String value) implements EventSourceId { @Override public String getValue() { return value; }}# Raises ArgumentError at compile time — the event source id is used to look# up the encryption key, so it cannot itself be an encrypted value.## defmodule MyApp.Compliance.Client.CustomerId do# use Chronicle.Concept, type: :uuid, event_source_id: true# pii()# endimport { pii } from '@cratis/chronicle';
// TypeScript represents the event source identifier as the conventional 'eventSourceId'// property rather than a dedicated EventSourceId<T> type. Marking it @pii() throws// PIINotSupportedOnEventSourceId - event source identifiers are required for key lookup and// cannot be encrypted.class ComplianceClientCustomerId { @pii() eventSourceId = '';}If the identifier itself is sensitive, use a non-sensitive surrogate key as the event source identifier (a randomly generated Guid works well) and store the sensitive value in a [PII]-marked event property.
Registering compliance services
Section titled “Registering compliance services”Compliance support is registered with a single extension method in your ASP.NET Core setup:
using Microsoft.Extensions.DependencyInjection;
public static class ComplianceClientRegistration{ public static void Configure(IServiceCollection services) => services.AddCompliance();}# No separate compliance registration step exists. Compliance metadata is# resolved automatically — from pii/1,2 declarations and from# Chronicle.Concept field types — whenever an event type or read model is# registered, as part of the normal Chronicle.Client supervision tree entry.{Chronicle.Client, connection_string: "chronicle://localhost:35000", event_store: "my-store", event_types: [MyApp.Compliance.Client.CustomerRegistered], read_models: [MyApp.Compliance.Client.Customer]}import { ChronicleClient } from '@cratis/chronicle';
// TypeScript has no DI-container registration step for compliance support - the PII manager// is available automatically as soon as you have an event store, with no separate wiring.async function getPIIManager(chronicleClient: ChronicleClient) { const eventStore = await chronicleClient.getEventStore('Sales'); return eventStore.pii;}This registers PIIMetadataProvider and all supporting infrastructure needed for the kernel to discover and encrypt PII properties.