Skip to content

FastAPI integration

effaced-fastapi compresses the integration to its irreducible parts: your annotated models, your auth, and one include_router call. The mechanical wiring — data map, subject graph, owned tables, audit sink, outbox, engines — happens once, inside EffacedStack.

Terminal window
uv add effaced effaced-fastapi
from effaced_fastapi import EffacedFastAPI, Subject
gdpr = EffacedFastAPI(base=Base, session_factory=session_factory,
resolvers=[StripeResolver(api_key=os.environ["STRIPE_API_KEY"])])
def current_subject(user: Annotated[User, Depends(current_user)]) -> Subject:
return Subject(subject_id=str(user.id),
refs=(SubjectRef(kind="stripe", value=user.stripe_customer_id),))
app = FastAPI(lifespan=gdpr.lifespan())
app.include_router(gdpr.router(subject=current_subject), prefix="/me")

That mounts:

RouteArticleEngine behind it
POST /me/consentArt. 7ConsentLedger.record — grant and withdrawal, same call
GET /me/consent/{purpose}Art. 7ConsentLedger.status
GET /me/exportArt. 15Exporter.export_subject
DELETE /meArt. 17ErasurePlanner.erase_subject

router(restriction=True) adds POST /me/restriction and GET /me/restriction (Art. 18 flag-keeping, never enforcement).

Annotating your models with pii() / subject_link() stays your job — that declaration is the integration. The runnable quickstart shows the whole thing end to end.

The router never authenticates. The subject dependency you pass — sync or async, chained on your real auth — answers two questions per request: who is the subject (the id your models key on) and where else do they live (the external refs whose kind routes to a resolver). A subject with no Stripe customer simply carries no stripe ref.

Each request runs in one transaction (session_factory.begin()): the local erasure phase and its outbox enqueue commit or roll back together — the atomicity the saga depends on. Override the session dependency per router (router(session=...)) or globally:

app.dependency_overrides[gdpr.session_dependency] = my_session_dependency

Routes are plain def functions: FastAPI runs the sync engines (ADR 0006) on its threadpool, so the event loop never blocks.

gdpr.lifespan() starts a SagaWorker — a daemon thread driving SagaRunner.run_once on its own private event loop, the pattern from the saga-runner guide — and stops it on shutdown. FastAPI takes a single lifespan: if your app already has one, construct SagaWorker inside it instead. Dedicated worker processes and cron remain just as valid; the worker is a packaged default, not a requirement.

A custom audit sink or a settings-driven registry means building the stack yourself and handing it over:

from effaced import EffacedStack, registry_from_settings
build = registry_from_settings(specs)
stack = EffacedStack.from_base(Base, session_factory, registry=build.registry)
gdpr = EffacedFastAPI(stack=stack)

Every wired engine stays reachable at gdpr.stack — e.g. gdpr.stack.rectifier for an Art. 16 route of your own. There is deliberately no built-in rectification endpoint: which corrections a subject may self-serve is an authorization decision your application owns (ADR 0020).

Route paths and response shapes are public API. Responses are the engines’ own result models (ExportBundle, ErasureResult, ConsentRecord), so endpoint payloads change only when engine behaviour does — and that is governed by widened SemVer.