Skip to content

feat: Vercel deployment webhook replaces steady-state status polling (TODO 5) - #118

Closed
lopugit wants to merge 1 commit into
mainfrom
claude/todo5-vercel-webhooks-s1
Closed

feat: Vercel deployment webhook replaces steady-state status polling (TODO 5)#118
lopugit wants to merge 1 commit into
mainfrom
claude/todo5-vercel-webhooks-s1

Conversation

@lopugit

@lopugit lopugit commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Implements TODO 5 — Replace Vercel status polling with Vercel webhooks.

What changes

Ready deployments were spending Vercel API calls on every footer poll just to detect a future build. Now:

  • POST /api/v1/vercel/webhook receives deployment lifecycle events (created / succeeded / promoted / error / canceled). The raw body is verified against VERCEL_WEBHOOK_SECRET (x-vercel-signature, HMAC-SHA1, timing-safe compare) before any parsing or DB work; the endpoint 404s entirely until the secret is configured; body capped at 256 KiB; unknown event types are acked with 200 so Vercel stops retrying them.
  • Persistence (webhookStatus.ts): latest event per branch stored as a singleton doc in the existing settings collection (vercelWebhookStatus:branch:<branch>) — no new collection. Out-of-order deliveries are dropped via an eventAt filter whose upsert insert collides with the unique key index (that duplicate-key error is the stale branch). Non-terminal states go stale after 30 min so a lost ready delivery can't pin the footer forever.
  • getVercelDeploymentStatus serves a terminal webhook state (ready/error/canceled) with zero Vercel API calls; a non-terminal state falls through to the existing API polling for live build-progress checks; tokenless deployments serve the webhook state either way. Polling remains the automatic fallback while no webhook event exists for the branch (i.e. before activation). Status responses gain source: 'webhook' for observability. The footer needs no changes — it reads the same endpoint.

Activation (deliberately left for the owner)

Creating a webhook is a standing Vercel account configuration change, so nothing registers it automatically. One-shot script:

VERCEL_API_TOKEN=… node remix/scripts/vercel/create-webhook.mjs \
  https://thingtime-lopugits-projects.vercel.app/api/v1/vercel/webhook

then set the printed VERCEL_WEBHOOK_SECRET in the Vercel project env and redeploy (documented in VERCEL_DEPLOYMENTS.md). Until then, behavior is byte-identical to today's polling.

Verification (live, worktree dev server, real Mongo — 16 checks, all pass)

  • unsigned + bad-signature deliveries → 401, no DB work
  • signed created → recorded queued; signed succeededready served with source: webhook, buildProgress: 100, deployment + inspector URLs surfaced
  • signed errorstate: error, hasError: true
  • out-of-order older event → dropped (recorded: false), status unchanged — verified both within a run and across runs
  • unknown event type → 200 acked, not recorded; GET → 405
  • non-terminal state with a real VERCEL_API_TOKEN present → correct fall-through to live polling (caught a real in-flight build from the parallel sessions)
  • in-app footer fetch: state: ready / source: webhook / progress: 100, zero console errors
  • test docs removed from the dev db afterwards

Session 1 of the parallel todo batch — claimed in TODO/SESSION-CLAIMS.md.

🤖 Generated with Claude Code

TODO item 5. Ready deployments were spending Vercel API calls on every
footer poll just to detect a future build. Now Vercel pushes deployment
lifecycle events and the status endpoint reads the persisted event instead.

- POST /api/v1/vercel/webhook: verifies x-vercel-signature (HMAC-SHA1 of the
  raw body with VERCEL_WEBHOOK_SECRET, timing-safe compare) before any
  parsing or DB work; 404s entirely until the secret is configured; caps the
  body at 256 KiB; acks unknown event types with 200 so Vercel stops
  retrying them. Registered in the Nitro API dispatcher.
- webhookStatus util persists the latest event per branch as a singleton doc
  in the existing `settings` collection (key vercelWebhookStatus:branch:<b>).
  Out-of-order deliveries are dropped via an eventAt guard (upsert filter +
  unique-key collision = stale branch). Non-terminal states go stale after
  30 min so a lost 'ready' delivery can't pin the footer.
- getVercelDeploymentStatus serves a terminal webhook state (ready / error /
  canceled) with zero Vercel API calls; a non-terminal state falls through
  to the API for live build progress; tokenless deployments serve the
  webhook state either way. Polling remains the fallback while no webhook
  event exists for the branch. Status gains source:'webhook' for
  observability.
- remix/scripts/vercel/create-webhook.mjs: owner-run one-shot that registers
  the webhook via the Vercel API and prints the signing secret to set as
  VERCEL_WEBHOOK_SECRET. Deliberately NOT run automatically - creating a
  webhook is a standing account configuration change.
- VERCEL_DEPLOYMENTS.md documents the flow and activation steps.

Verified live (worktree dev server, real Mongo, 16 checks all passing):
unsigned/bad-signature 401s, signed created/succeeded/error recorded and
served with correct state/progress/urls, out-of-order event dropped, unknown
type acked-not-recorded, GET 405, non-terminal fall-through to live polling
with a real token present, footer fetch shows state ready / source webhook /
progress 100 with zero console errors. Test docs cleaned from the dev db.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thingtime Ready Ready Preview, Comment Jul 21, 2026 4:42am

Request Review

@lopugit

lopugit commented Jul 21, 2026

Copy link
Copy Markdown
Owner Author

The 10-session parallel todo batch (2026-07-21) produced overlapping PRs before the shared claims file (TODO/SESSION-CLAIMS.md, untracked) reached every session. This is a per-todo map of the competing open PRs so review effort isn't spent twice. Facts, with recommendations only where sessions have posted actual diff comparisons.

Todo Competing PRs (size) Notes
9 Date-reviver corruption #94 (+25/-9), #96 (+28/-6), #98 (+37/-6), also bundled into #99 #94 vs #96 have a detailed comparison comment posted on both (formats compatible one-way #94#96; recommendation there: merge #96's tagged format, cherry-pick #94's TODO.md annotation). #98 arrived after both — likely redundant with whichever wins.
11 Feed ranked dedupe #93 (+34/-22), #95 (+17/-2) Same fix target; #95 is the smaller diff. Needs one winner.
12 PostCard memo #91 (+11/-10), #107 (+28/-13) #107 also covers other PostList consumers per its title; #91 is minimal.
13 Cmd/Ctrl+Z guard #101 (+17/-1) — resolved #108 (dup) already closed in #101's favor with live-verification evidence transferred.
6/07 Cross-tab sync #92 (+283/-15), #104 (+306/-3), #113 (+77/-1) Three independent BroadcastChannel implementations. #113 is far smaller — worth checking whether it covers the spec or is minimal-viable. High-conflict zone: all touch ThingtimeProvider.tsx alongside TODO 9/10 PRs.
10 eval revival + CSP #102 (+74/-42), bundled into #99 (+396/-131) #99 bundles TODO 8+9+10 in one PR — if #99 merges, #94/#96/#98/#102/#103/#106 partially collide; if the focused PRs merge, #99 is redundant. Decide bundle-vs-focused first.
8 remainder register/service-account hardening #100 (+114/-17), #103 (+20/-3), #106 (+39/-5), parts of #99 #100 targets service-account provisioning; #103/#106 target register — #103 and #106 likely overlap directly.
2 verification-link origin #105 (+26/-10) Base already fixed on main (resolveTrustedOrigin); #105 hardens the tokenless fallback with an allowlist. Independent, small.
4 introspection endpoint #114 (+231/-21), #116 (+166/-5), #117 (+171/-6) Three parallel implementations of POST /api/v1/auth/introspect. Needs one winner; compare scope handling + rate limiting.
14 debug-leak cleanup #110, #111, #112, #115 (all ~±30) Four near-identical cleanups; #109 additionally claims parts of 14 already shipped on main. Pick one, close three.
1 hydrateRoot #97 (+15/-5) Docs-only: closes the item as obsolete (createRoot architecture). Uncontested.
5 Vercel webhooks #118 (+320/-0) Uncontested. Activation is an owner-run script + VERCEL_WEBHOOK_SECRET env (standing-config change deliberately not automated).
3 HS256 removal Time-gated: ES256 shipped 2026-06-23; the 30-day cookie window ends ~2026-07-23. Not actionable yet.
15 DX ratchet (typecheck/test/CI) claimed by session 1, in progress Branch claude/todo15-dx-ratchet-s1.

Merge-order suggestion for the ThingtimeProvider.tsx conflict zone (todos 9, 10, 6): land the TODO 9 winner first (smallest, most urgent), rebase the TODO 10 winner on it, then the cross-tab winner — each of these edits the same reviver/persist region and will conflict otherwise.

Filed by session 1. Claims registry: TODO/SESSION-CLAIMS.md (untracked, main checkout).

@lopugit

lopugit commented Jul 21, 2026

Copy link
Copy Markdown
Owner Author

Coordination note: PR #122 implements the same TODO #5 (webhook-fed Vercel status) from a parallel session — these two overlap and only one should merge. Flagging so the duplicate isn't merged twice.

@lopugit

lopugit commented Jul 21, 2026

Copy link
Copy Markdown
Owner Author

Closing in favor of #122 (session 1 → the duplicate resolution both coordination comments asked for).

Honest comparison: the two implementations agree on everything load-bearing — HMAC-SHA1 raw-body verification with timingSafeEqual, 404-until-secret-configured, persistence in the settings collection, terminal-states-served-with-zero-API-calls / building-falls-through-to-live-poll, out-of-order-delivery guards, and owner-approval-gated activation. #122 additionally bounds the store at 30 branches (my per-branch docs grew unbounded with preview churn), carries lastReadyAt/lastReadyUrl across builds, and adds the /docs/api entry — user-visible wins, so it should be the one that merges even though #118 was opened ~3 minutes earlier.

Two things worth cherry-picking from #118 into #122 (or a follow-up):

  1. One-command activation instead of dashboard clicking — remix/scripts/vercel/create-webhook.mjs from this branch (claude/todo5-vercel-webhooks-s1, commit 3dddaa0) registers the webhook via POST /v1/webhooks with the project/team defaults and prints the signing secret exactly once with setup instructions. Grab the file directly from the branch.
  2. My verification additionally covered the cross-run out-of-order case (a later runner invocation replaying an older eventAt is dropped by the persistence guard, not just within-run ordering) — worth a quick check against feat(vercel): webhook-fed deployment status — stop polling Vercel for ready deployments (TODO 5) #122's per-deployment superseding logic, which keys staleness differently.

Verification evidence from this PR that transfers (same design): 16 live checks including unsigned/tampered 401s before any DB work, ready/error served with source:'webhook' + zero Vercel API calls, non-terminal fall-through hitting a real in-flight build, footer fetch green with no console errors.

@lopugit lopugit closed this Jul 21, 2026
lopugit added a commit that referenced this pull request Jul 21, 2026
…t.promoted handling

Cherry-picked from session 1's closed duplicate PR #118 (branch
claude/todo5-vercel-webhooks-s1, credit where due): the owner-run
scripts/vercel/create-webhook.mjs registers the webhook via POST /v1/webhooks
and prints the signing secret exactly once; deployment.promoted now maps to
ready so promotions recorded by that event set are tracked. Live-verified:
promoted event -> ready; cross-run stale replay for a superseded deployment
still ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lopugit added a commit that referenced this pull request Aug 8, 2026
…t.promoted handling

Cherry-picked from session 1's closed duplicate PR #118 (branch
claude/todo5-vercel-webhooks-s1, credit where due): the owner-run
scripts/vercel/create-webhook.mjs registers the webhook via POST /v1/webhooks
and prints the signing secret exactly once; deployment.promoted now maps to
ready so promotions recorded by that event set are tracked. Live-verified:
promoted event -> ready; cross-run stale replay for a superseded deployment
still ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lopugit added a commit that referenced this pull request Aug 9, 2026
…t.promoted handling

Cherry-picked from session 1's closed duplicate PR #118 (branch
claude/todo5-vercel-webhooks-s1, credit where due): the owner-run
scripts/vercel/create-webhook.mjs registers the webhook via POST /v1/webhooks
and prints the signing secret exactly once; deployment.promoted now maps to
ready so promotions recorded by that event set are tracked. Live-verified:
promoted event -> ready; cross-run stale replay for a superseded deployment
still ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lopugit added a commit that referenced this pull request Aug 9, 2026
…t.promoted handling

Cherry-picked from session 1's closed duplicate PR #118 (branch
claude/todo5-vercel-webhooks-s1, credit where due): the owner-run
scripts/vercel/create-webhook.mjs registers the webhook via POST /v1/webhooks
and prints the signing secret exactly once; deployment.promoted now maps to
ready so promotions recorded by that event set are tracked. Live-verified:
promoted event -> ready; cross-run stale replay for a superseded deployment
still ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lopugit added a commit that referenced this pull request Aug 17, 2026
…t.promoted handling

Cherry-picked from session 1's closed duplicate PR #118 (branch
claude/todo5-vercel-webhooks-s1, credit where due): the owner-run
scripts/vercel/create-webhook.mjs registers the webhook via POST /v1/webhooks
and prints the signing secret exactly once; deployment.promoted now maps to
ready so promotions recorded by that event set are tracked. Live-verified:
promoted event -> ready; cross-run stale replay for a superseded deployment
still ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant