PII survives in related tables.
The user row is deleted; the addresses, support tickets, and the Stripe customer object live on. Nobody declared them, so nobody erased them.
EXHIBIT 1 · JOINED TABLES, THIRD PARTIESCASE FILE EF-2026-0611 RE: RIGHT TO ERASURE DISPOSITION: GRANTED
effaced ships the GDPR data-subject mechanisms — export (Art. 15), erasure (Art. 17), consent (Art. 7), and an append-only audit trail (Art. 5(2)) — across your own database and the external systems you actually use. Stripe first.
$uv add effaced effaced-stripe Pre-alpha — not yet on PyPI. The short command lands with 0.1.0; until then, Copy gives you the working install-from-GitHub one-liner.
We ship the mechanisms. You own the compliance.
The user row is deleted; the addresses, support tickets, and the Stripe customer object live on. Nobody declared them, so nobody erased them.
EXHIBIT 1 · JOINED TABLES, THIRD PARTIESInvoices carry statutory retention duties. A naive cascade deletes them anyway — trading one legal problem for another.
EXHIBIT 2 · RETENTION VS. CASCADE
Art. 5(2) expects you to demonstrate what happened. A bare DELETE leaves
nothing to show — success and silent failure look identical.
One structured bundle of everything a subject's data touches — local tables and resolvers. Failures never silently shrink the bundle; they are recorded as incomplete sources.
ART. 17Delete or anonymize in FK-safe order, skip and record what the law makes you keep, and enqueue every external deletion durably — all in one atomic transaction.
ART. 7An append-only ledger where withdrawing is exactly as easy as granting — one call, one immutable record, mirrored into the audit trail before your commit.
ART. 5(2)Append-only by construction — there is no update or delete surface to misuse. Every erasure, export, and consent change leaves a PII-free, replayable record.
Annotate the models you already have — there is no separate config file to drift out of sync.
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from effaced import ErasureStrategy, PiiCategory, RetentionPolicy, pii, subject_link
class User(Base):
__tablename__ = "users"
__table_args__ = {"info": subject_link("")} # this IS the data subject
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(info=pii(PiiCategory.CONTACT))
class Invoice(Base):
__tablename__ = "invoices"
__table_args__ = {"info": subject_link("user")} # reaches the subject via .user
billing_address: Mapped[str] = mapped_column(
info=pii(
PiiCategory.FINANCIAL,
erasure=ErasureStrategy.RETAIN, # legally retained — never deleted,
retention=RetentionPolicy(reason="§147 AO"), # and the audit trail says why
)
) The entire integration surface is three calls.
data_map = collect_data_map(Base.metadata)
graph = resolve_subject_graph(data_map, Base.registry)
tables = bind_tables(Base.metadata) # effaced-owned tables ride your migrations
audit = DatabaseAuditSink(session_factory, tables.audit_events)
registry = ResolverRegistry()
registry.register(StripeResolver(api_key="rk_live_...")) # the registry doubles as
# your "where is my PII" list
ConsentLedger(tables.consent_records, audit).record(session, record) # Art. 7
Exporter(data_map, graph, Base.metadata, audit, registry).export_subject(
session, user_id, refs=(stripe_ref,)
) # Art. 15
ErasurePlanner(data_map, graph, registry).erase_subject(session, user_id) # Art. 17 External APIs cannot join your database transaction. effaced doesn't pretend they can: the local phase commits atomically, and every external deletion is enqueued in that same transaction — then fanned out by a runner you already operate.
When Stripe is down mid-erasure, you don't have a half-erased mystery. You have a committed local state, a durable queue, retries with backoff — and an entry that keeps failing is abandoned loudly: audited and surfaced, never silently dropped.
| Alternative | The gap |
|---|---|
| Roll your own | Misses PII in related tables and third parties; deletes retained invoices; no Art. 5(2) record; breaks mid-flight when an API is down. |
| django-gdpr-assist | Closest prior art — archived since ~2022, Django-only, local ORM only. effaced is the maintained successor for the modern Python stack. |
| DSR platforms | OneTrust, Transcend et al. are heavy, expensive, DPO-facing SaaS — not a drop-in developer library. |
| GDPR boilerplates | Shallow download/delete buttons in a template — not reusable machinery with an audit trail. |
Including what effaced is not — read the full comparison.
effaced gives you correct machinery to implement Articles 15, 17, 7, and 30 — and an auditable record that you did. Whether your processing is lawful is a legal determination only you and your counsel can make.
Pre-alpha 0.x, under widened SemVer: any change to what gets deleted or exported is a MAJOR version, regardless of syntax. Silently changing behaviour like that is the worst failure a library like this can have — see stability.