From 735b2daedf1d25d9635de5bae2e1d9113aaa368a Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Tue, 14 Jul 2026 20:47:58 +0200 Subject: [PATCH] docs(users): warn against wiring a users trigger to the non-transactional emit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `users_service` still write-then-emits at nine call sites. That is currently INERT — `triggers.kind` has no `users` value, so the outbox emitter has no arm for the source and drops the event — but it is the exact shape audit #6 removed from every other stateful service, and it would be reintroduced by whoever adds a users trigger kind and finds the plumbing already there. Note that this one does not even log the failure (`let _ = ...`). Documented at the emit site, pointing at `atomic_write` and the write-path invariant in CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/manager-core/src/users_service.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/manager-core/src/users_service.rs b/crates/manager-core/src/users_service.rs index 51846a7..b4f82a5 100644 --- a/crates/manager-core/src/users_service.rs +++ b/crates/manager-core/src/users_service.rs @@ -302,6 +302,19 @@ impl UsersServiceImpl { Ok(self.roles.list_for_user(app_id, user_id).await?) } + /// Emit a `users` lifecycle event. + /// + /// **Currently inert, and deliberately so.** `triggers.kind` has no `users` + /// value, so `outbox_event_emitter::plan()` has no arm for this source and + /// drops the event. These calls exist so the plumbing is already in place if + /// a users trigger kind lands. + /// + /// **If you add one, do NOT wire it up here.** This is the non-transactional + /// write-then-emit shape that audit #6 removed from every other stateful + /// service: the row commits, and if the outbox insert then fails the trigger + /// silently never fires (note the `let _ =` — this one does not even log). + /// Route it through a `crate::atomic_write` writer instead, so the row and + /// its fan-out commit together. See the write-path invariant in CLAUDE.md. async fn emit(&self, cx: &SdkCallCx, op: &'static str, user_id: AppUserId) { let _ = self .events