Embed update hook: the corpus tells its embeds
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.
erDiagram access_codes ||--o| embeds : "exposed by" embeds |o--o| webhook_endpoints : "update hook"
What standmeet.com does with it
sequenceDiagram
autonumber
actor Owner
participant SM as sijie.xyz (StandMeet)
participant W as standmeet.com Worker
participant KV as Workers KV
actor V as Visitor
Owner->>SM: edit a note in the standmeet subtree
SM->>SM: trigger → outbox → relay → webhook.fanout
SM->>SM: EmbedAdmits, inside LANDING-BLOG's code scope ✓
SM->>W: webhook.deliver POST /api/corpus-hook (signed)
W->>W: verify signature (±5 min), skip a webhook-id seen in the last 24 h
W->>SM: GET /api/v1/corpus-cards (with updated_at)
W->>KV: diff against the index, find changed / new / gone
loop each change × zh/en
W->>SM: GET /api/v1/wiki/{path}?lang=
W->>KV: store the note
end
W->>KV: delete gone notes, update the index, remember the webhook-id
W-->>SM: 2xx
V->>W: GET /zh/blog/…
W->>KV: read note + index
W-->>V: HTML rendered per request (links resolved against the current index)
- 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.