CASE FILE EF-2026-0611 RE: RIGHT TO ERASURE DISPOSITION: GRANTED

Personal data, provably gone.

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.

SUBJECT RECORD · USERS №1284 EXHIBIT A
email jane.doe@example.com
stripe cus_9XK42MQ7L1
invoice №881 Hauptstr. 14, MünchenRETAINED · §147 AO · REASON RECORDED
audit ERASURE_COMPLETED · RECORDED
ERASED · AUDITED
01

Hand-rolled erasure fails the same three ways.

MISSED

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 PARTIES
OVERDELETED

Legally retained records get hard-deleted.

Invoices carry statutory retention duties. A naive cascade deletes them anyway — trading one legal problem for another.

EXHIBIT 2 · RETENTION VS. CASCADE
UNRECORDED

No defensible record of any of it.

Art. 5(2) expects you to demonstrate what happened. A bare DELETE leaves nothing to show — success and silent failure look identical.

EXHIBIT 3 · ACCOUNTABILITY GAP
03

The annotations are the data map.

STEP 1

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
        )
    )
STEP 2

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
04

Erasure is a saga, not a function call.

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.

How the saga works →

erase_subject runs one atomic database transaction that deletes or anonymizes in FK-safe order, records retained fields, and enqueues outbox entries. A saga runner you operate fans the outbox out to Stripe with retries; the audit trail records every outcome, including abandonment. erase_subject(session, user_id) ONE ATOMIC TRANSACTION ├─ DELETE / ANONYMIZE · FK-SAFE ORDER ├─ LEGALLY RETAINED FIELDS · SKIPPED + RECORDED └─ OUTBOX ENTRIES · ENQUEUED IN THE SAME TXN DURABLE OUTBOX SAGA RUNNER · YOUR WORKER / CRON STRIPE · DELETE CUSTOMER RETRY W/ BACKOFF "ALREADY GONE" = SUCCESS AUDIT TRAIL · APPEND-ONLY EVERY OUTCOME RECORDED ABANDONMENT IS NEVER SILENT
FIG. 1 — ERASURE AS A SAGA
05

An honest comparison.

AlternativeThe 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.