inspeximus · reference · updated 2026-07-31

LangGraph store: GDPR erasure with a verifiable receipt

Deleting a key from a BaseStore removes it from your reads. It does not give you anything to hand an auditor. This page shows the difference, with code you can run and the actual output it produces — including the part where we say what the receipt does not prove.

The gap

LangGraph's long-term memory lives behind BaseStore: put, get, search, delete. A deletion request under GDPR Article 17 asks for something the interface has no word for — evidence, after the fact, that a specific subject's data is gone, produced without restating the data you just erased.

There is a second, quieter problem. A namespace is LangGraph's per-user boundary, so "erase this user" means "erase this namespace" — but a store that resolves a subject by string matching can take a neighbour down with it. That has to be exact, or an erasure becomes an incident.

Runnable

pip install "inspeximus[langgraph]"
from inspeximus.integrations.langgraph import InspeximusStore

store = InspeximusStore(path="memories.json", receipts=True)

store.put(("users", "alice"), "profile", {"email": "alice@example.com"})
store.put(("users", "alice"), "prefs",   {"tone": "concise"})
store.put(("users", "bob"),   "profile", {"email": "bob@example.com"})

# the DSAR: exact, resolved from the stored namespace rather than a joined string
store.erase_namespace(("users", "alice"), request_id="dsar-2026-0731")

cert = store.store.erasure_certificate("dsar-2026-0731")   # the REQUEST id, not the subject

It is an ordinary BaseStore: everything LangGraph expects works unchanged, and erase_namespace is the addition.

What actually comes back

scoped_to      : dsar-2026-0731
count          : 2
request_ids    : ['dsar-2026-0731']
erased ids     : ['23f19e720e', '765d0c7d35']
self_check     : {"verified": true, "problems": []}
anchor         : n_writes=3  n_tombstones=2
tombstones_tip : c84cc0cc46b96dbd7caa938174969321b092c340...

alice gone     : True
bob intact     : {'email': 'bob@example.com'}
verify_writes  : (True, [])

Three things are worth reading carefully in that output.

What this does not prove

This is the part the certificate itself states, and it is the reason to trust the rest of it. From the scope field, verbatim: erasure is within this store only — not the app's vector store, prompt logs, or backups; it covers the subject plus its derived_from lineage. It is a tamper-evident integrity primitive. It is not a legal opinion, and it is not a media-level unrecoverability claim.

That distinction is not pedantry. We measured a delete that returned success and left the data recoverable in five of six places one application had put it — its own vector index, its cache, its logs. The store was the one place that had actually erased. If your app copied the value anywhere else, this receipt says nothing about that copy, and you should register those stores as erasure targets so the manifest names them.

One limitation of ours, stated

There are two erasure paths and they are not equivalent. erase_namespace() is exact. store.forget_subject("lg::users::alice") resolves by the per-record subject string, which is "lg::" + "::".join(namespace) — lossy, so ("a","b") and ("a::b",) share it and a request naming one takes both. We kept the lossy form deliberately, because changing it would re-address every record written by an earlier version and make those records invisible to a DSAR; the precise path is the new method rather than a silent change to the old one. Use erase_namespace() for anything that matters.

Verifying later

from inspeximus.audit_bundle import verify_bundle
# the store's own check, re-run at any time:
store.store.verify_writes()          # -> (True, [])

The certificate re-derives the chain from genesis, so it can be checked by someone who has the file and none of your code. Given the store, it also confirms every id it names is genuinely absent — a certificate that lists an id still present in the store fails its own check.