How to Build a GDPR-Compliant RAG Deletion Pipeline
A GDPR deletion request is not a single database delete. A RAG system stores customer data in at least five places. Missing one means the deletion is incomplete — and a 72-hour SLA breach is a regulatory event.
The Valid Failure
Before Book 4, ShopBot's deletion process was manual. When a tenant customer filed a GDPR deletion request, a developer received an email, looked through five systems, deleted what they could find, and sent a confirmation.
The first compliance audit found that the process took an average of 4 hours and 12 minutes. The GDPR requirement is 72 hours — so the timeline was technically met. But the audit also found that two previous deletions had missed the RAGAS evaluation dataset: a labeled CSV file that contained customer session transcripts. The system had been logging real queries to build the evaluation set, and the CSV had not been included in the deletion inventory.
No deletion process is reliable unless it is automated and covers a complete, written-down inventory of every store that holds customer data.
The 5-Store Deletion Inventory
For ShopBot's six-tenant architecture, a customer's data lived in five places:
| Store | What it holds | Deletion mechanism |
|---|---|---|
| Qdrant (per-tenant collection) | Chunk payloads with session metadata | delete by filter on customer_id hash |
| Redis | Session memory (active conversations) | delete by session key |
| LangGraph state | In-flight pipeline state | Flush on session close |
| RAGAS evaluation dataset | Labeled query-answer pairs | Row deletion from CSV/database |
| Audit log (DynamoDB) | Immutable query events | Cannot delete — SHA-256 hash resolves this |
The audit log conflict — how do you delete from an immutable log? — is the architecturally interesting problem.
Resolving the Audit Log Conflict
The audit log must be immutable (tamper-evident, regulatory requirement). It also must not contain PII that is subject to deletion (GDPR requirement).
The resolution: store a SHA-256 hash of the customer identifier, not the identifier itself.
import hashlib
def tenant_scoped_hash(customer_id: str, tenant_id: str) -> str:
"""One-way hash that is unique per (customer, tenant) pair."""
raw = f"{tenant_id}:{customer_id}"
return hashlib.sha256(raw.encode()).hexdigest()
# Usage
customer_hash = tenant_scoped_hash("user@example.com", "vastralaya")
# → "a3f8d2c1..." (irreversible)
# Store in audit log — no email address, no PII
audit_event = {
"customer_hash": customer_hash, # not the email
"tenant_id": "vastralaya",
"query_fingerprint": hashlib.sha256(query.encode()).hexdigest(),
"timestamp": datetime.utcnow().isoformat(),
"event_type": "QUERY",
}
The SHA-256 hash is not reversible. Deleting the email address from other systems destroys the link between the hash and the person. The audit log entry remains — but it no longer identifies anyone. The deletion is GDPR-compliant.
The Automated Deletion Pipeline
import boto3
import redis
import hashlib
from datetime import datetime, timezone
r = redis.Redis()
dynamodb = boto3.resource("dynamodb", region_name="ap-south-1")
def delete_customer_data(customer_id: str, tenant_id: str) -> dict:
customer_hash = tenant_scoped_hash(customer_id, tenant_id)
collection = f"shopbot_{tenant_id}"
results = {}
started_at = datetime.now(timezone.utc)
# 1. Qdrant — delete chunks tagged with this customer's session IDs
from qdrant_client.models import Filter, FieldCondition, MatchValue
qdrant_client.delete(
collection_name=collection,
points_selector=Filter(must=[
FieldCondition(key="customer_hash", match=MatchValue(value=customer_hash))
]),
)
results["qdrant"] = "deleted"
# 2. Redis — delete all session keys for this customer
session_pattern = f"session:{tenant_id}:{customer_hash}:*"
for key in r.scan_iter(session_pattern):
r.delete(key)
results["redis"] = "deleted"
# 3. LangGraph state — already gone (TTL on session close)
results["langgraph"] = "ttl_expired"
# 4. RAGAS evaluation dataset
remove_from_ragas_dataset(customer_hash, tenant_id)
results["ragas_dataset"] = "deleted"
# 5. Audit log — no PII stored, hash only, nothing to delete
results["audit_log"] = "no_pii_stored_sha256_only"
completed_at = datetime.now(timezone.utc)
elapsed = (completed_at - started_at).total_seconds()
# Issue deletion receipt
receipt = {
"customer_hash": customer_hash,
"tenant_id": tenant_id,
"requested_at": started_at.isoformat(),
"completed_at": completed_at.isoformat(),
"elapsed_seconds": elapsed,
"stores_processed": results,
"status": "COMPLETE",
}
# Write receipt to audit table (non-PII)
audit_table = dynamodb.Table("shopbot-audit")
audit_table.put_item(Item={
**receipt,
"event_type": "GDPR_DELETION_RECEIPT",
})
return receipt
The Deletion Receipt
The deletion receipt is the evidence that the process ran and completed. It is stored in the audit table (non-PII — it contains only the customer hash, not the customer identifier).
{
"customer_hash": "a3f8d2c1...",
"tenant_id": "vastralaya",
"requested_at": "2026-07-28T09:14:22Z",
"completed_at": "2026-07-28T09:17:36Z",
"elapsed_seconds": 194,
"stores_processed": {
"qdrant": "deleted",
"redis": "deleted",
"langgraph": "ttl_expired",
"ragas_dataset": "deleted",
"audit_log": "no_pii_stored_sha256_only"
},
"status": "COMPLETE"
}
In Book 4, the deletion pipeline ran in 3 minutes and 14 seconds. The 72-hour GDPR window was met with 71 hours and 57 minutes to spare.
The Valid Knowledge
- Write the inventory first: before building the pipeline, list every system that stores any customer data. The deletion pipeline is only as complete as the inventory.
- SHA-256 + tenant scoping resolves the immutable log conflict: the hash is irreversible. When the email is deleted from other systems, the hash becomes an orphan — it no longer identifies anyone.
- Tenant scoping prevents hash collision across tenants:
sha256("vastralaya:user@example.com")≠sha256("jaipur_threads:user@example.com"). The same customer using two tenants has two different hashes — their cross-tenant identity is not linkable from the hash alone. - The receipt is regulatory evidence: when an audit asks "prove you deleted this customer's data within 72 hours," the receipt is the answer. Store it.
- 72 hours is the maximum, not the target: the pipeline should run in minutes. The 72-hour window is for edge cases (manual investigation, disputed requests). The automated path should complete in under 5 minutes.