Code structure: the transaction is passed explicitly
Parent: events
Status: released in v0.1.76 (2026-09-27) — design and as-built record in docs/design/event-bus-outbox-webhooks.md in the StandMeet repo.
Most of the bus is "infra behind interfaces" (queue-behind-ports). Three things needed a decision: how a transaction reaches the use case, how subscribers register with zero glue, and where the side-effect ports moved.
How a transaction reaches the use case
Transactions used to be opened inside repos, and most writes were single autocommit statements, so use cases held no transaction and an explicit Record could not join the business write. Trigger-captured row events (two-sources-of-events) are unaffected; semantic events are.
The transaction is an explicit parameter, never a ctx value. ctx already carries 14 WithValue keys. Putting the transaction there too would add a hidden dependency — whether a repo call joins a transaction would depend on what the caller's ctx happens to hold — and that is how ctx becomes a god object.
pgstore.InTx(ctx, db, fn)begins a transaction ondb, aBeginner: the pool, or an open transaction (then it is a savepoint). It commits whenfnreturns nil and rolls back otherwise; a panic infnrolls back and re-panics.pgstore.Nested(q, pool)is where a repo bound byWith(q)opens its own transaction: a savepoint insideqwhenqis a transaction, else a transaction of its own on the pool.With(tx)on every repo,RecorderandJobsreturns a copy bound to that transaction. Existing method signatures do not change. The compiler checks who is in the transaction.ctxcarries cancellation and deadlines only. This plan adds noctxkey.Recordtakes the owner and other inputs as explicit arguments, never fromctx.- Gate
check-tx-only-via-pgstore.sh:.Begin(,BeginTx(andBeginFunc(may appear only ininternal/infra/pgstore. The 11 existingBeginsites were converted (the plan had estimated about 20) (no-bypass-by-structure). - UTs: an error or panic in
fnrolls back business rows, event and job together; aWith(tx)copy does not alter the original; calls withoutWithbehave as before (events-test-plan).
Subscribers with zero glue
Each domain declares its event types (event-model), subscriptions and job kinds as data, the same shape as dispatcher ops, and exposes them through its facade. The composition root (cmd/server/wire/periodic.go) collects them with one line per source and says nothing about what a job does. The only dedicated wiring is the embed-scope function (EmbedAdmits, from the access domain) handed to the owner domain's webhook fan-out.
Side-effect ports moved
OutboundSender lived in cmd/server/port and the background supplier call in plugin/adapters. The mail port is now internal/infra/sideeffect/mail; the background supplier call is the durable supplier.invoke job in internal/infra/sideeffect/supplier. Their error tables, lint exemptions and test hooks moved with them. Only subscriber packages, infra and cmd/server may import them.