GDPR Erasure
End-to-end walkthrough of crypto-erasure for GDPR Article 17 compliance.
This guide walks through a complete GDPR Article 17 (right to erasure) implementation using Chronicle's crypto-erasure system: recording events with subject IDs, triggering erasure, and verifying the hash chain remains intact.
What Chronicle's erasure does
- Associates events with a data subject via
SubjectID. - Encrypts sensitive fields with a per-subject AES-256-GCM key stored in
KeyStore. - On erasure: destroys the key — making encrypted data irrecoverable.
- Marks affected events as
Erased = truein the store. - Records an
erasure.Erasurerecord for audit and compliance purposes. - The hash chain stays structurally valid (hashes cover metadata, not encrypted payload).
Step 1: Enable crypto-erasure
c, err := chronicle.New(
chronicle.WithStore(adapter),
chronicle.WithCryptoErasure(true),
)Or via the extension:
ext := extension.New(
extension.WithStore(pgStore),
extension.WithCryptoErasure(true),
)Step 2: Record events with a SubjectID
ctx = scope.WithAppID(ctx, "myapp")
ctx = scope.WithTenantID(ctx, "tenant-1")
// Record several events for user "alice"
for _, action := range []string{"profile_view", "email_change", "export_data"} {
err = c.Info(ctx, action, "user", "alice").
Category("account").
SubjectID("alice").
Meta("email", "alice@example.com").
Record()
if err != nil {
log.Fatal(err)
}
}Step 3: Verify the events exist
import "github.com/xraph/chronicle/audit"
result, err := c.Query(ctx, &audit.Query{
Category: "account",
Limit: 20,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("events before erasure: %d\n", len(result.Events))
// events before erasure: 3Step 4: Erase via the HTTP API
POST /v1/erasures
Content-Type: application/json
{
"subject_id": "alice",
"reason": "GDPR Article 17 — right to erasure",
"requested_by": "dpo@company.com"
}Response:
{
"id": "erasure_01j9vk...",
"subject_id": "alice",
"events_affected": 3,
"key_destroyed": true
}Step 4 (alternative): Erase programmatically
import "github.com/xraph/chronicle/erasure"
svc := erasure.NewService(pgStore, keyStore)
result, err := svc.Erase(ctx, &erasure.Input{
SubjectID: "alice",
Reason: "GDPR Article 17",
RequestedBy: "dpo@company.com",
}, "myapp", "tenant-1")
if err != nil {
log.Fatal(err)
}
fmt.Printf("key_destroyed=%v events_affected=%d\n",
result.KeyDestroyed, result.EventsAffected)
// key_destroyed=true events_affected=3Step 5: Verify events are marked as erased
result, err = c.Query(ctx, &audit.Query{Category: "account", Limit: 20})
if err != nil {
log.Fatal(err)
}
for _, ev := range result.Events {
fmt.Printf("event=%s erased=%v\n", ev.ID, ev.Erased)
}
// event=audit_01j9vk... erased=true
// event=audit_01j9vk... erased=true
// event=audit_01j9vk... erased=trueStep 6: Verify the hash chain is still valid
import "github.com/xraph/chronicle/verify"
vReport, err := c.VerifyChain(ctx, &verify.Input{
AppID: "myapp",
TenantID: "tenant-1",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("valid=%v verified=%d gaps=%v tampered=%v\n",
vReport.Valid, vReport.Verified, vReport.Gaps, vReport.Tampered)
// valid=true verified=3 gaps=[] tampered=[]The chain is valid because hashes are computed over event metadata (action, resource, timestamps), not the encrypted payload.
Production key store
The crypto.NewInMemoryKeyStore() is suitable for development only. For production, implement the crypto.KeyStore interface backed by AWS KMS, HashiCorp Vault, or a database:
type KeyStore interface {
GetOrCreate(subjectID string) (key []byte, keyID string, err error)
Get(subjectID string) (key []byte, err error)
Delete(subjectID string) error
}The key store is separate from the Chronicle store — Chronicle does not manage it. Provision it yourself and pass it to erasure.NewService.
Erasure record retention
Erasure records (erasure.Erasure) are not automatically purged by retention policies — they are compliance records. Store them for as long as your compliance framework requires (typically at least 7 years).