Compliance · GDPR right-to-erasure
executeErasureRequest — the structural primitive for Article 17 erasure against an append-only hash chain. Reads the fundamental tension honestly; surfaces both approaches.
GDPR Article 17 ("right to be forgotten") obligates the controller to
erase personal data on a qualifying request. Append-only hash chains
— the substrate that gives harness_event_log cryptographic
tamper-evidence — are fundamentally at odds with row-level deletion.
This page explains the tension, the two production approaches, and
the structural primitive @pleach/compliance ships for one of them.
Shipped in an earlier release.
The fundamental tension
A hash chain's integrity depends on the exact bytes of every row
participating in the next row's row_hash. Erase one row in the
middle of the chain and every downstream row's prev_hash no longer
matches the prior row's row_hash — the chain breaks for the entire
suffix.
A naive DELETE on a PII row, then, is structurally indistinguishable
from substrate tamper. Any honest GDPR-compliant deployment of a hash
chain has to choose between two reconciliation strategies, neither of
which is free.
Procurement conversations should NOT skip this distinction.
Approach (a) — Crypto-shredding (recommended for production)
PII fields are stored ENCRYPTED at write time with a per-subject key (typically AES-GCM with a per-subject KEK in a managed KMS). Erasure deletes the per-subject key — the encrypted blob remains in place, the row hash is unchanged, the chain stays valid, and the data is cryptographically inaccessible.
Pros: Chain integrity preserved indefinitely; pre-erasure attestations continue to verify; matches the deployment model auditors expect for highly regulated workloads.
Cons: Requires upstream KEK management infrastructure
(@aws-sdk/client-kms, GCP KMS, Azure Key Vault) and per-field
envelope encryption discipline at every write site. Higher onboarding
cost.
This SKU does not ship crypto-shredding today. AEAD field encryption + OOB key shred lives in a future release or in @pleach/compliance-pii once the AEAD envelope contract is locked.
Approach (b) — Row deletion + chain rebuild (this slice's primitive)
PII rows are physically deleted; downstream rows are re-anchored against a fresh genesis seed. The rebuilt chain has a DIFFERENT genesis root than the pre-erasure chain.
Pros: No upstream KEK infrastructure required; works against any
event store with a DELETE capability.
Cons: Pre-erasure attestations no longer verify against the post-erasure chain. This is a feature, not a bug — the genesis-root change IS the cryptographic proof of erasure. A verifier comparing pre-erasure and post-erasure roots sees the divergence and concludes "erasure happened here."
Operator MUST accept this trade-off explicitly. Any deployment that needs both Article 17 + immutable pre-erasure verifiable attestations should prefer crypto-shredding (approach (a)).
executeErasureRequest — the structural primitive
import { executeErasureRequest } from "@pleach/compliance/hashchain";
const result = await executeErasureRequest({
request: {
tenantId: "tenant-acme",
subjectId: "user@example.com",
scope: "all",
reason: "DSAR-2026-06-15 Art 17(1)(a)",
operator: "support-engineer@example.com",
},
eventStore: {
async eraseRows(filter) {
return await pgEraseRowsForSubject(pool, filter);
},
},
rebuildChainFromGenesis: async (tenantId, chatId) => {
return await rebuildChainInPostgres(pool, tenantId, chatId);
},
chatIds: ["chat-1", "chat-2"], // optional; defaults to [subjectId]
});
console.log(result);
// {
// rowsErased: 142,
// chainValidAfter: true,
// attestation: {
// timestamp: "2026-06-15T10:42:01.123Z",
// reason: "DSAR-2026-06-15 Art 17(1)(a)",
// operator: "support-engineer@example.com",
// },
// }ErasureScope
| Scope | Coverage |
|---|---|
"messages" | Every row tied to user-authored messages (content.delta, content.finalized payloads carrying user prose). |
"calls" | Every row tied to LLM call traces (llm.turn, tool.invoked). |
"all" | Every event-log row for the subject within the tenant. |
The mapping from scope to a concrete filter is the adapter's
responsibility. ErasureEventStoreLike.eraseRows receives the scope
along with tenantId + subjectId and decides which event_types
participate. This keeps @pleach/compliance vendor-neutral — the
package has no opinion about your event-type taxonomy.
Injected callbacks — why the package stays vendor-neutral
Both eventStore.eraseRows and rebuildChainFromGenesis are
consumer-supplied structural callbacks. The actual erasure is a
database operation (Postgres DELETE, Supabase RLS-scoped soft-delete,
or per-row tombstone); the chain rebuild is a substrate-specific
recompute that walks every downstream row and re-canonicalizes it.
Neither is portable across substrates, so neither belongs in the
package. The structural primitive sequences the two operations,
aggregates per-chat rebuild verdicts into the final chainValidAfter
flag, and seals an attestation footer.
Soft-fail vs throw
- Adapter error on
eraseRows— soft-failed. The helper returnsrowsErased: 0,chainValidAfter: false, and an attestation tagged<erasure-failed-soft>. Chain integrity breaks are the only throw case. - Rebuild callback throws — soft-failed per chat. The helper sets
chainValidAfter: falseand continues to the next chat. ChainVerificationErrorfrom either callback — re-thrown. Chain integrity breaks are hard failures by design.
What the attestation proves
interface ErasureAttestation {
readonly timestamp: string; // ISO-8601 seal time
readonly reason: string; // operator-supplied audit reason
readonly operator?: string; // operator id
}The attestation is the audit footer the operator presents downstream:
"erasure executed at timestamp, scoped to request.scope, under
lawful basis request.reason, by operator." Append-only — the
helper does not retain the attestation; it is the operator's
responsibility to persist it in the compliance ledger.
The attestation is INTENTIONALLY minimal. It does NOT carry the new
chain root or the count of rows erased — those are returned alongside
in ErasureResult and persisted separately by the operator. A future
slice may extend the attestation footer with a chain-root pointer; for
now the operator wires the cross-reference.
End-to-end example
import { Pool } from "pg";
import { executeErasureRequest } from "@pleach/compliance/hashchain";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
async function handleDsarRequest(
tenantId: string,
subjectEmail: string,
caseId: string,
operatorEmail: string,
) {
// 1. Resolve which chats the subject participated in. Adapter-level
// decision — typically a join against a per-tenant subject->chat
// table.
const chatIds = await resolveSubjectChats(pool, tenantId, subjectEmail);
// 2. Execute the erasure + chain rebuild.
const result = await executeErasureRequest({
request: {
tenantId,
subjectId: subjectEmail,
scope: "all",
reason: `${caseId} Art 17(1)(a)`,
operator: operatorEmail,
},
eventStore: {
eraseRows: async (filter) => pgEraseSubjectRows(pool, filter),
},
rebuildChainFromGenesis: async (t, c) =>
pgRebuildChainFromGenesis(pool, t, c),
chatIds,
});
// 3. Persist the attestation footer in the compliance ledger.
await persistAttestation(pool, {
caseId,
erasureResult: result,
});
return result;
}Cited source
packages/compliance/src/hashchain/erasure.ts—executeErasureRequestfactory.packages/compliance/src/hashchain/index.ts— barrel re-export.packages/compliance/test/gdprErasure.smoke.test.mjs— surface regression-lock.
Where to go next
HashChainMiddleware
The companion helper that decorates each event with chain anchors before persistence.
Hash chain
Substrate-level walkthrough of @pleach/core's chain primitives.
Compliance
The full @pleach/compliance surface — redaction, scrubbers, audit query, PDF export.
Regulated domain agent
End-to-end recipe composing compliance + hash chain + erasure.
Compliance · HashChainMiddleware
Consumer-facing wire-up between the host event-log writer and @pleach/core's hash-chain primitives. beforeWrite decorates each event; verifyAfterBatch checks substrate integrity.
Sandbox providers
The SandboxProvider facade over @pleach/sandbox's three endpoint-shape adapters, plus the SandboxComposite high-level tool surface coding agents reach for.