From 95b6e3a1c9ffdbc368d5832b985eae1e85c7a0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tailland?= Date: Sat, 29 Aug 2026 13:49:18 +0200 Subject: [PATCH] docs: the events are emitted by the API, and what that implies Recording where the work lands and the two facts about that codebase that shape it: every write passes through one BaseModelViewSet, so one hook covers every domain; and the models are managed = False, so a transactional outbox introduces the estate's first Django-managed table beside the pre-existing schema. Emission belongs in the API rather than a database CDC or a gateway plugin, because only the API knows which change is a business event and only it holds the sub/azp that the event must carry for a downstream agent to act under the same authority. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8cc0c4f..e5ecb90 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -96,15 +96,38 @@ policy must not need to know the trigger mode. ## Backlog -**Events over Kafka.** The target for the reactive half. The estate emits no -domain event today — `classic-models-api` has no signals, no outbound webhook, -and there is no bus in the cluster — so a workflow can only poll, which is what -made a nightly sweep look like the only option. Until Kafka is in place, -detection stays unbuilt rather than being approximated by a periodic scan: a -batch that pretends to be reactive is worse than an absent feature, because it -looks finished. +**Events over Kafka, emitted by `classic-models-api`.** The target for the +reactive half. The estate emits no domain event today — the API has no signals +and no outbound webhook, and there is no bus in the cluster — which is what made +a nightly sweep look like the only option. -The periodic ordering run does not wait on this. +The emission belongs **in the API**, not in a database CDC and not in a gateway +plugin. Only the API knows which change is a business event rather than a row +update, and only it holds the identity that made it — `sub` and `azp` from the +caller's token, which the event has to carry if a downstream agent is to act +under the same authority as the person who triggered it. + +Two facts about that codebase shape the work: + +- Every write goes through one `BaseModelViewSet` in + `api/v1/classicmodels/views.py`. One hook there covers products, orders, + payments and the rest — Django signals scattered across models would be the + harder version of the same thing. +- **The models are `managed = False`.** The tables are the pre-existing + classicmodels schema and Django owns no migrations for them. A transactional + outbox means introducing the estate's first managed table beside unmanaged + ones — which works, and is worth deciding deliberately rather than + discovering. + +An outbox is the part that matters: emitting from a viewset after `save()` drops +the event if the process dies between the two, and an agent that never hears +about a stock movement is worse than one that hears late. + +Until this exists, detection stays unbuilt rather than being approximated by a +periodic scan: a batch that pretends to be reactive is worse than an absent +feature, because it looks finished. + +The periodic ordering run does not wait on any of it. ## Two controls that become mandatory here