feat: add webhooks via Cloudflare Queues and Cron Triggers#142
Open
ephraimduncan wants to merge 6 commits into
Open
feat: add webhooks via Cloudflare Queues and Cron Triggers#142ephraimduncan wants to merge 6 commits into
ephraimduncan wants to merge 6 commits into
Conversation
Deliver submission.created webhooks using Cloudflare Queues (delivery, retry/backoff, dead-letter queue) and Cron Triggers (safety-net sweep and log cleanup) — no Redis, BullMQ, or separately deployed worker, replacing the approach abandoned in #121. - schema: webhook_delivery_logs table + forms.enable_webhook / webhook_url / webhook_secret (migration 0003) - producer: submission route and form.testWebhook enqueue via WEBHOOK_QUEUE, deferred with after() (never a floating promise) - consumer/cron: custom OpenNext worker entry (worker.ts) exporting queue() and scheduled(); process.env hydrated before dynamic imports so the workerd handlers can reach the validated env - HMAC-SHA256 request signing via WebCrypto with X-Formbase-Signature, X-Formbase-Event and X-Formbase-Timestamp headers and a per-form secret - UI: webhook settings, read-only signing secret, and recent deliveries list - SSRF-hardened URL validation; signing secret is never exposed through the public getFormById procedure - cron sweep atomically leases stuck rows to avoid duplicate delivery
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
formbase-web | 5222cad | Commit Preview URL Branch Preview URL |
Jun 06 2026, 08:30 PM |
Contributor
|
sweet |
Address low-severity review findings: - consumer: wrap DLQ markFailed in try/finally so the message is always acked even if the status write throws (prevents a stranded DLQ batch) - isValidWebhookUrl: close SSRF denylist gaps — full 127.0.0.0/8, 0.0.0.0, bracket-stripped IPv6 loopback/ULA/link-local, and integer/hex IP encodings - form settings: gate the Test button on the live URL field + dirty state (and reset after save) so it can't test a stale, unsaved URL
Next 16 builds with Turbopack, which refuses to follow symlinks that resolve outside the project root. Two things broke the build in this bun monorepo: apps/web resolves dependencies from the hoisted root node_modules (a level above apps/web), and bun's isolated linker also symlinked some deps (@base-ui/react, @tailus/themer, tailwind plugins) into its global cache outside the repo entirely. - next.config.js: set outputFileTracingRoot + turbopack.root to the monorepo root so Turbopack can resolve the root node_modules - bunfig.toml: use the hoisted install linker so every dependency lives inside the repo (no global-cache symlinks for Turbopack to reject)
- Use space-y-2 for the webhook signing-secret block to match sibling sections - Drop stray pnpm from db:migrate so it runs tsx directly like the other db scripts
aikins01
suggested changes
Jun 4, 2026
aikins01
left a comment
Contributor
There was a problem hiding this comment.
thanks for pushing this forward. i found a few blockers that need fixing before this can merge: CI is failing, the public form lookup exposes webhook targets, and the stuck-delivery sweep can overrun queue batch limits.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds form webhooks — when a submission arrives, formbase POSTs it to a user-configured URL with retries — built entirely on Cloudflare-native primitives. This replaces the approach in #121 (BullMQ + Redis + a separately deployed Fly.io worker), which required an always-on external service.
How it works
The custom OpenNext worker entry (
apps/web/worker.ts) re-exports the generatedfetchhandler + cache DOs and addsqueue()/scheduled(). It hydratesprocess.envfrom the bindings before dynamically importing the consumer/scheduled modules, so the workerd handlers can reach the validated env (whichnext dev's request context normally provides).What's better than #121
timestamp.body, sent asX-Formbase-Signature/X-Formbase-Event/X-Formbase-Timestamp, with a per-formwebhook_secret.after()(the pattern that caused the submission hang fixed in fix: prevent submission endpoint hang on Cloudflare Workers #141), notvoid promise.webhook_delivery_logstable in feat: add webhook queue system and worker #121 was never surfaced).idis a stable idempotency key for receivers.getFormByIdprocedure (explicit column allowlist).UPDATE … SET next_retry_at … RETURNING) so it can't double-deliver rows still in the queue's own retry window.Changes
webhook_delivery_logstable;forms.enable_webhook/webhook_url/webhook_secret; migration0003_awesome_inhumans.producer,deliver(HMAC),consumer(backoff + DLQ),scheduled(sweep + cleanup).form.updatewebhook fields + server-generated secret,form.testWebhook,form.listDeliveries;setFormDatanow returns{ id }; queue threaded through context.wrangler.jsoncqueues/DLQ/cron +main→./worker.ts;cloudflare-env.d.tsgitignored;tsconfigcheckJs:false.apps/worker/andpackages/queue/leftovers from feat: add webhook queue system and worker #121.Verification
@formbase/db,@formbase/utils,@formbase/api: typecheck clean.Before deploying
wrangler queues create formbase-webhooksandwrangler queues create formbase-webhooks-dlq.packages/db→db:migrate).wrangler dev/opennextjs-cloudflare preview(notnext dev); validate end-to-end against awebhook.siteURL.Known follow-ups (low severity)
markFailedcould be wrapped intry/finallybeforeack().isValidWebhookUrldenylist has IPv6/numeric-host gaps (backstopped byglobal_fetch_strictly_public).