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

consolidation-inventory

Consolidation inventory: 101 items

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.

A full sweep of backend, app, sdk, builder, im-bridge, infra/plugins and the updater found 101 items of hand-wired "after X, also do Y", and each one got exactly one target. Targets: E emit an event plus a subscriber, J durable job, P periodic job on River, W exposed as a webhook event type, K keep. Row numbers refer to the sweep (as of origin/main 6eb8c0a64). The "As built" column says where each move landed. Phases are in events-roadmap.

Moves: after-write side effects

#WhatWhere it wasFailed byTargetAs built
1Access request → email the owneraccess_requests.go:59 → request_notify.go:44blocked the request through retries; lost on failure; Redis throttle dropped silentlyE access_request.created + Jowner.notify (owner/subscriber/mail.go); the 5-per-hour burst cap is a Postgres slot and stays a deliberate drop
2Approve request → mail the code + mark repliedaccess_approval.go:86-89mail sent but status not written = inconsistentE + Jaccess_request.approval_mail enqueued with the code in one transaction; replied written after the send
4Email-change confirmationemail_change.go:138failed send left a dangling pending rowJowner.email_confirmation; the link token is minted at send time
5Booking → notify ownerbooker-mcp.js:458 → invoke_background.go:53detached goroutine, lost on restartE booking.created + Jthe booking.record host op writes booking.* and a booking_notices row in one transaction; owner.notify sends and deletes it
7Booking persist failed → delete calendar event (compensation)booker-mcp.js:309,337failed delete left an orphan event, never retriedJdurable supplier.invoke
8–10Meili upsert / delete / reindex after publishcorpus_crud.go, subjectivity.go, corpus.go:139, output.go:61, seo.go:126,140skipped calls, slow requests, process-local dirty flagE corpus.note.changed + Jcorpus_notes trigger + corpus.index (P1)
11Full rebuild after Obsidian importobsidian.go:115,133goroutine, lost on restartJper-note trigger events; goroutine deleted (P1)
14Build settled → auto-publish homepagebuilds.go:242failure only logged, never retriedE microsite.build.settled + Jowner.SettleBuild writes the build row, the event and pg_notify('standmeet_build_settled', owner) in one transaction; subscriber microsite.homepage_publish
15Build settled → recompute asset refsbuilds.go:247,256sameE + Jsubscriber microsite.asset_refs
16Build settled → wake preview long-pollbuilds.go:193 → buildnotifyin-process broadcast, broke with replicasE (LISTEN/NOTIFY)pgstore.Listener keyed by owner; the version is the durable ms timestamp of the owner's latest settle; infra/buildnotify deleted

Moves: goroutines, periodic jobs, calls inside requests

#WhatWhere it wasFailed byTargetAs built
22InvokeBackground (background supplier call + retry)invoke_background.go:49-65lost on restart, no dead letter, single processJ supplier.invokeinternal/infra/sideeffect/supplier; invoke_background.go and notifyPolicy deleted (P4)
23Obsidian rebuild goroutineobsidian.go:135same as #11Jdeleted (P1)
30The periodic scheduler itselfperiodic.go:47-92ran on every replica; state in memoryPRiver periodic jobs (leader, run on start); the in-process scheduler deleted (P1)
31Meili 8 s reconcilecorpus_index_periodic.go:40relied on a process-local flagdeletedeleted (P1)
32–36public conversation prune, gas refill, resume-draft sweep, traffic retention, usage cleanupeach *_periodic.goran on every replicaPdeclared as data, run on River (P1)
38Boot-time Meili index + full backfillwire/search_index.go:22slowed bootJcorpus.reindex enqueued at boot (P1)
39, 62Job-source fetch (all sources serially inside the request)sources_write.go:133, jobfetch.go:149slow; could hit the 30 s write timeoutone J per source + E jobs.fetchedjobs.fetch_source on the fetch queue; jobs.fetch_result. No scheduled fetch: job-loop.md rejects an automatic daily fetch (P4)
63Outbound mail (retry.Do inside the request)mail_retry.go:49, outbound_sender.go:103request blocked through backoffJ (covers #1, #2, #4)mail port internal/infra/sideeffect/mail; internal/infra/retry deleted (P4)
98Mail throttlemailthrottle.go:82dropped silentlythrottle hit snoozesthe per-recipient cap (30/h) snoozes to the next window (P4)
101Background-jobs list in the monitor paneljobreg_registry.go:24reset on restartreads River jobsJobRegistry deleted; instance.jobs reads River (tasks-panel) (P1)

Client polling → pushed events (Phase 5, not built)

#PollingWhereIntervalTarget
50Microsite list long-polluse-microsites.ts:127held requestSSE
51Poll a build until settleduse-microsites.ts:2901.5 sSSE
52Sidebar access-request badgeuse-sidebar-badges.ts:3960 sSSE
54Wait for Google Calendar after OAuthuse-gcal.ts:1611 s × 15SSE
56SDK recovery of a dropped turnagent-adapters.ts:1861 s × 6SSE

Webhook event types (all thin; none subscribed by default)

The sweep listed 23 rows. As declared they are 37 types, plus corpus.note.changed and webhook.test: 39 in all, every one Webhook-exposed (event-model). See webhooks for delivery.

#EventsPhase
75–76access_request.created / .approved / .status_changedP2
77–78code.issued / .revoked / .redeemedP2
79–82conversation.started / .message / .pruned, ghost.acceptedP2
83booking.created / .cancelled / .rescheduledP4
84–85application.committed, jobs.fetchedP4
86–88writing.published / .unpublished, corpus.note.changed, vault.importedP2
89–90microsite.build.settled, page.promoted_live / .rolled_back / .unpublished, microsite.store.doc_insertedP4
91–93api_key.issued / .revoked, supplier.connected / .disconnected / .activated, block.installed / .failedP2
94–97gas.exhausted / .refilled, instance.upgrade_requested, owner.login / .email_changed / .recovery_requested, ip_ban.addedP2

Keep (K)

KindItems
Writes into the same database that are themselves the source of truth#12 cross-link rebuild, #13 import receipt, #17 block-failure record, #19 inference usage, #20 dialog, card and ghost writes, #21 seen job ids
Process plumbing and servers#24–#29
Per-node sandbox cleanup#37
The updater's file signal and polling (it deliberately does not touch the database)#44–#46
Telegram long-polling and in-memory im-bridge sessions#48–#49
External calls whose result a visitor needs on the spot (now sent once, no in-request retry)#64 calendar, #66 media fetch, #67 model listing, #68 spec validation, #71 OAuth, #72 captcha, #73 system probes, #74 inference
Session and cache invalidation#99–#100
Waiting for restart after an upgrade (the server is down during the restart)#55
Traffic recording (decided)#18

Open items, resolved as proposed

#ItemResolutionWhy
3Recovery-phrase emailK, synchronousThe user waits on the login page and must learn on the spot whether it was sent
6Visitor booking confirmation emailK, synchronousThe visitor needs the confirmation now; the compensating delete moves to J (#7)
41–43Build queue (hand-written SKIP LOCKED + lease)K for the queue; its settle hooks go through EThe builder is a cross-process Node sidecar and the queue is already durable; little gain from moving it
47im-bridge polls its config every 15 sK; SSE considered in P5The config rarely changes
53System info refreshed every 1 sKLive metrics are naturally polled
57Visitor tools "appear on the next poll"find the caller before P5The sweep did not find the client call site
59–61PDF render for application commit, draft, public reportK, synchronousThe user wants that PDF now; commit renders first on purpose
69Marketplace installKThe result is needed on the spot
18Traffic recordingKDecided
75–97Webhook exposureall subscribable, none by default, all thinThese are facts of the owner's own instance; security events suit alerting

Fixed along the way (not in the plan)

  • SDK hydration: BlockWidget and use-chat-session read localStorage in the first render (React #418 on prerendered microsites); they now read it after mount.
  • A data race on plugin/mount knownToolSpecs; Meili Index takes a fresh index handle per call; Tasks detail acted on a stale job after switching rows.
  • Three places where the wiki was right and the code wrong: schema.sql lacked visit_event / visit_viewer (found by the schema parity UT); a stale Typst comment in drafts_edit.go; the manual-upgrade copy named a nonexistent STANDMEET_REDEPLOY_HOOK (8 locales).

Four of the moved operations had callers that waited for the result; their completion signals are in completion-hooks and async-response-contract.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →