Embed update hook: the corpus tells its embeds
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 standmeet.com side is built in its own repo and not deployed yet.
An embed's update hook is a webhook endpoint attached to the embed. Its scope is the embed's access code. When a note inside that scope changes, the consuming site learns it and re-reads only what moved.
Where the idea came from
standmeet.com shows the owner's corpus as a blog through an embed. It had no way to learn that the corpus changed: the inventory listed "embed consumers (standmeet.com)" with no mechanism at all. The update hook is Phase 3 of the event-bus plan, and standmeet.com is its first consumer.
One field, one attached endpoint
embeds.create and embeds.update take update_hook_url; the embed form has an Update hook URL field. Filling it creates or updates the endpoint attached to that embed (the upsert has its own UT):
event_types = ["corpus.note.changed"].embed_idis set, so the scope is the embed's code scope: role globs minus the code's denials. The access domain answers it (EmbedAdmits) with the sameentity.AllowsCorpusEntrypredicate every read uses. A revoked or expired code, or a deleted embed, admits nothing.raw://never leaves.- The response carries
update_hook {endpoint_id, url}, and the signing secret only when the endpoint was just created. - The payload is thin. The receiver reads bodies through the same scoped API the embed already uses. For how an embed authenticates without carrying the code, see embed-credential-never-carries-the-code.
/api/v1/corpus-cardscarriesupdated_at(second precision), so the receiver can tell which cards moved.
What standmeet.com does with it
- Blog pages, RSS, the blog sitemap and "latest notes" render per request from Workers KV (Astro Cloudflare adapter).
prerender = falseis set only on the blog routes; "latest notes" on the landing page is a server island. POST /api/corpus-hookverifies the Standard Webhooks signature with a ±5 min timestamp window, dedupes onwebhook-id(kept 24 h in KV), re-lists the cards and fetches only entries whoseupdated_atmoved, in both languages.- Links are resolved at request time against the current index, so a rename or delete does not leave stale links.
- The first request fills an empty KV from the instance (bootstrap). There is no cron.
- Deploy needs the KV namespace id and
CORPUS_HOOK_SECRET. - The standmeet.com side lives in a separate repo (
atmaxmoj/standmeet-landing).
No reconcile cron
The owner decided against any reconcile loop. Delivery is guaranteed or visibly failed: outbox, durable jobs and panel alerts cover it (message-loss-guarantees). A reconcile loop hides unreliable delivery; with reliable delivery it is not needed.
Acceptance
- e2e
embed-update-hook(passing): the embed form's hook → edit a note → the sink receives it; cards carryupdated_at; an out-of-scope edit does not reach the hook (sentinel form). - Real environment (open): the owner edits a note on sijie.xyz → within 60 s the standmeet.com page shows the change, without a deploy. Recorded under
docs/real-env-verification/after the standmeet.com deploy. See events-roadmap · events-test-plan.