⚠️ Important difference from Next.js
queue: true does NOT work on Cloudflare Workers. Always use explicit queue config with the env parameter.
diff --git a/app/frameworks/cloudflare/page.tsx b/app/frameworks/cloudflare/page.tsx
new file mode 100644
index 0000000..6947744
--- /dev/null
+++ b/app/frameworks/cloudflare/page.tsx
@@ -0,0 +1,65 @@
+import { SiteNav } from "@/components/site-nav";
+
+export default function CloudflareFrameworkPage() {
+ return (
+ Framework — Cloudflare Workers Web Crypto API native. Zero Node.js dependencies. Runs in V8 isolates across Cloudflare's global network. queue: true does NOT work on Cloudflare Workers. Always use explicit queue config with the env parameter. Framework — Next.js app router Purpose-built App Router adapter. Reads platform and secret from Vercel feature flags at runtime. Switch platforms without redeploying. Modes Call 1: Platform → Tern verify + enqueue + 200. Call 2: QStash → Tern verify QStash signature + run handler.Webhook verification at the edge.
+ ⚠️ Important difference from Next.js
{`import { createWebhookHandler } from '@hookflo/tern/cloudflare'
+
+export interface Env {
+ WEBHOOK_SECRET: string
+ QSTASH_TOKEN: string
+ QSTASH_CURRENT_SIGNING_KEY: string
+ QSTASH_NEXT_SIGNING_KEY: string
+ [key: string]: unknown
+}
+
+export default {
+ async fetch(request: Request, env: Env): PromiseSecrets setup
{`WEBHOOK_SECRET=whsec_xxx
+QSTASH_TOKEN=qstash_xxx
+
+npx wrangler secret put WEBHOOK_SECRET
+npx wrangler secret put QSTASH_TOKEN
+npx wrangler secret put QSTASH_CURRENT_SIGNING_KEY
+npx wrangler secret put QSTASH_NEXT_SIGNING_KEY`}Deploy
{`npx wrangler dev
+npx wrangler deploy`}Test locally
Error reference
The webhook handler Next.js was missing.
+ $ npm i @hookflo/tern
{`// app/api/webhooks/route.ts
+import { createWebhookHandler } from '@hookflo/tern/nextjs'
+
+export const POST = createWebhookHandler({
+ platform: 'stripe',
+ secret: process.env.WEBHOOK_SECRET!,
+ handler: async (payload) => {
+ return { received: true }
+ }
+})
+
+export const GET = async () => {
+ const failed = await controls.dlq()
+ return Response.json({ count: failed.length, events: failed })
+}
+
+export const PATCH = async (request: Request) => {
+ const { dlqId } = await request.json()
+ const result = await controls.replay(dlqId)
+ return Response.json(result)
+}`}
+ Switch platforms with a flag flip.
+ {`import { createWebhookHandler } from '@hookflo/tern/nextjs'
+import { platform } from '../flags'
+
+export const POST = createWebhookHandler({
+ platform: await platform(),
+ secret: process.env.WEBHOOK_SECRET!,
+ handler: async (payload) => {
+ return { received: true }
+ }
+})`}
+ Environment variables
{`WEBHOOK_SECRET=whsec_xxx
+QSTASH_TOKEN=qstash_xxx
+QSTASH_CURRENT_SIGNING_KEY=sig_xxx
+QSTASH_NEXT_SIGNING_KEY=sig_xxx`}Two-call pattern
Test locally
Error reference
- Tern is a zero-dependency TypeScript framework for webhook - signature verification. One SDK, every platform. No boilerplate, - no fragile hand-rolled crypto. -
- -- Clerk webhook verification today vs with Tern. Same security, a - fraction of the code. -
- -- No code change. No redeployment. Set your platform via Vercel - feature flags — Tern reads them at runtime. Switch from Clerk to - Stripe to GitHub without touching your codebase. -
-- Purpose-built adapters for every major runtime. Same verification - logic, native integration. -
-{mw.desc}
-Tern webhook security
++ Verify signatures across platforms, then enqueue verified events for retries, dead-letter queue recovery, and + deduplication with Bring Your Own Keys (BYOK). +
+- Verified implementations — not guesswork. Each platform is tested - against real webhook payloads. -
-- ● verified Custom config lets you verify any{" "} - HMAC-based webhook without waiting for built-in support. -
-- No magic, no bloat. Just the right abstractions in the right places. -
-platform from Vercel
- feature flags. Change platforms at runtime — zero code
- changes, zero redeployments.
- Beyond verification
+01
+One handler API for Stripe, Clerk, GitHub, Fal AI, Replicate, and more.
+02 New
+Queue, retry, DLQ, replay, and deduplication powered by Upstash QStash.
+ + Open Reliable Delivery → + +03
+Purpose-built adapters for Next.js App Router and Cloudflare Workers.
+ + Read Next.js guide → + +- Open source. MIT licensed. Built and maintained at Hookflo. -
-Platform guides
+Ed25519 signature flow, webhook event filtering, and low-cost testing setup.
+ + +Fetch webhook secret via API, verify Ed25519 signatures, and test predictions.
+Platform — Fal AI
+Unlike Stripe or Clerk, Fal AI signs with Ed25519 public key cryptography. Use secret: "" intentionally and tern fetches public JWKS automatically.
{`curl -X POST \\
+ 'https://queue.fal.run/fal-ai/flux/schnell?fal_webhook=YOUR_URL' \\
+ -H "Authorization: Key $FAL_KEY" \\
+ -H "Content-Type: application/json" \\
+ -d '{"prompt": "a red cat", "num_images": 1}'`}
+ Fal AI sends two webhook events: IN_QUEUE and COMPLETED. Filter with fal_webhook_events=completed.
Cheapest model for testing: fal-ai/flux/schnell — $0.003 per image.
+Platform — Replicate
+{`curl -s \\
+ -H "Authorization: Bearer $REPLICATE_API_TOKEN" \\
+ https://api.replicate.com/v1/webhooks/default/secret`}
+ Response includes whsec_* key. Store that as REPLICATE_WEBHOOK_SECRET once.
{`curl -X POST \\
+ -H "Authorization: Bearer $REPLICATE_API_TOKEN" \\
+ -H "Content-Type: application/json" \\
+ -d '{
+ "version": "YOUR_MODEL_VERSION",
+ "input": { "prompt": "a red cat" },
+ "webhook": "YOUR_URL",
+ "webhook_events_filter": ["completed"]
+ }' \\
+ https://api.replicate.com/v1/predictions`}
+ Replicate uses Ed25519 signatures. tern handles verification when secret is configured.
+Platform — Stripe
+Use tern to verify Stripe signatures, then add queue mode for retries and DLQ controls.
+01 — Powered by Upstash QStash
++ Every verified webhook is queued, retried on failure, deduplicated, and recoverable from a dead-letter queue. + Bring Your Own Keys (BYOK). Zero infrastructure. Your data never leaves your stack. +
+Tern never touches your Upstash credentials. Pass your QStash token and events flow through your account directly. No vendor lock-in. No data through our servers.
+QSTASH_TOKEN=qstash_••••••••••
+ Flow
+Queue features
+01
QStash retries failed deliveries with exponential backoff.
queue: {'{ retries: 3 }'}02
Events exhausting retries land in your DLQ for replay.
controls.dlq()03
Repeated webhook event IDs are processed once.
Upstash-Deduplication-Id04
Replay any failed message with one API call.
controls.replay(dlqId)Code
+{`import { createWebhookHandler, controls } from '@hookflo/tern/nextjs'
+
+export const POST = createWebhookHandler({
+ platform: 'stripe',
+ secret: process.env.WEBHOOK_SECRET!,
+ queue: true,
+ handler: async (payload) => {
+ return { received: true }
+ }
+})
+
+export const GET = async () => {
+ const failed = await controls.dlq()
+ return Response.json({ count: failed.length, events: failed })
+}
+
+export const PATCH = async (request: Request) => {
+ const { dlqId } = await request.json()
+ const result = await controls.replay(dlqId)
+ return Response.json(result)
+}`}
+ queue: true → Next.js only, reads env automatically.
+queue: {'{ ... }'} → all frameworks, explicit config.
+| Base fee/month | $39 | $0 |
| 500K events | $44/mo | $5/mo |
| 5M events | $89/mo | $50/mo |
| Annual saving | — | $468–$528/yr |
Tern is free. You pay Upstash directly.
+✓ 1,000 messages/day free · ✓ No credit card required · ✓ Upgrade only when you need to.
+$ npm i @hookflo/tern
+ Open source · MIT · Zero dependencies
+