Embed update hook: the corpus tells its embeds

Updated · View the entry on sijie.xyz ↗

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_id is 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 same entity.AllowsCorpusEntry predicate 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-cards carries updated_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 = false is set only on the blog routes; "latest notes" on the landing page is a server island.
  • POST /api/corpus-hook verifies the Standard Webhooks signature with a ±5 min timestamp window, dedupes on webhook-id (kept 24 h in KV), re-lists the cards and fetches only entries whose updated_at moved, 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 carry updated_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.