2026-09-27·by Sijie Wang#standmeet#architecture#design#events

events-roadmap

Roadmap and decisions

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.

The bus shipped in phases P0 to P4; P5 is not scheduled. Each phase was test-first: its e2e specs were seen red on the unchanged code (make test-asis) before any implementation. The webhook sink is a set of routes on the external mock (mock-stack/job-board/webhook_sink.go): it records every request with its headers and body, can be told to fail or delay (/__mock/set_delay), and sits in EGRESS_ALLOW_HOSTS for e2e only. What exists is in events-test-plan.

Shipped: the full-suite acceptance passed on 2026-09-27; the release, v0.1.76, is live on sijie.xyz since 2026-09-27.

Still open: the standmeet.com deploy and the P3 real-environment check (a note edit shows on standmeet.com within 60 s).

PhaseContentAcceptance (e2e, black-box)Status
P0Adopt River v0.47River's own migrator (jobsriver.Migrate) runs at boot right after pgstore.Migrate; schema.sql does not copy River's DDL; the worker starts and stops with the server contextdone
P1events table + corpus_notes trigger + relay + subscriber registry; corpus.index replaces the 9 hook calls, the SEO reindex and the Obsidian goroutine; the 8 s reconcile loop and the dirty flag are deleted; periodic jobs move to River; the Tasks panelevents-index-via-bus, events-bulk-import-bound, tasks-panel, tasks-panel-more, upgrade-events-outboxdone
P2endpoints table, ops, admin section, signing, SSRF guard, retry schedule, cooldown, auto-disable, delivery log, re-deliver, webhook.test; all 39 types exposedwebhooks, webhook-event-types, events-fault-injectiondone
P3the embed form's update hook; cards gain updated_at; standmeet.com KV + /api/corpus-hookembed-update-hook (backend); edit a note on sijie.xyz → within 60 s the standmeet.com page updates, no deploy (real environment, recorded)backend done; standmeet.com deploy and the 60 s real-environment check open
P4access-request mail, approval mail, email confirmation, booking notification (booking.record), supplier.invoke, build-settled signal, per-source job fetch → all through the bus or durable jobsevents-side-effects-durable, events-build-settled; the call-site diff is net deletiondone
P5plugin subscriptions (cordis ctx.on), activity feed as an event projection, live admin updates over SSE, IM push—not scheduled

P3 is the embed-update-hook; P2 is webhooks; what each phase moved is itemized in consolidation-inventory. Phase 1 also carries the storage-bounds and the tasks-panel.

Decided (2026-09-26)

The owner adopted every recommendation.

DecisionOutcomeWhy
Queue implementationRiver, behind the Jobs / Inspector / Runtime interfaces (queue-behind-ports)Proven in production; no new service (why-not-a-broker); MPL-2.0 is compatible with AGPL; swappable later without upper-layer changes
Row-change captureDatabase triggers with a WHEN clause; semantic events stay explicit Record calls (two-sources-of-events)Coverage is total by construction; a skipped call cannot happen
PayloadThin (type, subject, ids)Nothing private leaves the instance; one source of truth
Off the busMonitor and traffic recordingHigh volume, no consumer; moving it would double the write load
Reconcile loopsNone: no cron on standmeet.com, and the Meili 8 s loop is deletedOutbox plus durable jobs plus panel alerts already guarantee delivery or make failure visible. A reconcile loop hides unreliable delivery; with reliable delivery it is not needed (message-loss-guarantees).
Open inventory itemsResolved as proposed in consolidation-inventory—

Decided during the build

DecisionOutcomeWhy
CoalescingPer-subscription opt-in; only corpus.index coalescesThe index re-reads current state; a webhook or mail delivers a fact, and merging two facts loses one (relay-claims-rows-not-cursor)
RelayA loop in every process plus a 1-minute sweep, not a River jobSKIP LOCKED prevents double fan-out without a leader; a frequent periodic job would fill river_job; the sweep covers a lost NOTIFY
One delivery per endpointA lease row (busy_until), not pg_try_advisory_xact_lockAn advisory transaction lock would hold a transaction across the POST (concurrency-control)
Owner-notify burst capStays a drop (5 per owner per hour), now a Postgres slotA flood must not mail the owner each time; requests stay visible in admin; a retry keeps its slot
Synchronous supplier callsSent once; the in-request retry.Do is goneOne retry owner; a visitor-facing call answers fast (retry-has-one-owner)
Completionpg_notify on terminal state; no job.completed outbox eventsOne outbox event per job would fan out again for every job (completion-hooks)
Fetch resultjobs.fetch_result returns the listings; tasks.get shows state onlyThe Tasks ops stay generic
Scheduled fetchNonedocs/design/job-loop.md rejects an automatic daily fetch
Gate self-testsNone kept; each gate shown red once on a planted sampleA gate is a gate (no-bypass-by-structure)
events-roadmap