Tern
×
Upstash
Upstash QStash
Reliable Delivery

From fire-and-forget
to guaranteed delivery.

Every verified webhook is queued, retried on failure, deduplicated, and recoverable from a dead-letter queue. Bring Your Own Keys (BYOK) — your Upstash account, zero infrastructure. Your data never leaves your stack.

Get started →View on GitHub
Hero preview
Bring Your Own Keys

Tern never manages your Upstash credentials. Pass your QStash token — your events flow through your account directly. No vendor lock-in. No data routed through Hookflo's servers.

QSTASH_TOKEN=qstash_••••••••••
QSTASH_CURRENT_SIGNING_KEY=sig_••••••••
QSTASH_NEXT_SIGNING_KEY=sig_••••••••
your keys · your Upstash account · your data
Get your keys from Upstash Console →
Retries
configurable
0
Infra
to manage
1
Config
option
100%
Sovereignty
data stays yours

Every failure mode has
a defined recovery path.

No event gets silently lost. Signature invalid → non-retryable. QStash down → retryable. Handler fails → backoff. Exhausted → DLQ.

Incoming
Stripe / Clerk / GitHub
Verify
Signature check
Enqueue
QStash BYOK
Handler
Your logic
Retry
Backoff
DLQ
Exhausted

Verification was just
the beginning.

01
Automatic Retries
QStash retries failed deliveries with exponential backoff. Configure retry count or use your plan default. Handler down? It'll try again.
queue: { retries: 3 }
02
Dead-Letter Queue
Events exhausting all retries land in your DLQ — never lost forever. Inspect programmatically and replay any event at any time.
controls.dlq()
03
Deduplication
Same event arriving twice — from Stripe retries or network hiccups — processed exactly once. Per-platform ID resolution with SHA-256 fallback.
built-in · Upstash-Deduplication-Id
04
Programmatic Replay
Replay any DLQ message with a single API call. No dashboard required. Full control from your code — inspect, filter, and replay on demand.
controls.replay(dlqId)

Two options,
one install.

queue: true is zero-config for Next.js on Vercel. Use explicit config for Cloudflare Workers, custom env names, or non-Vercel deploys.

$ npm i @hookflo/tern
queue: true reads QSTASH_TOKEN, QSTASH_CURRENT_SIGNING_KEY, and QSTASH_NEXT_SIGNING_KEY from your environment automatically. All three must be set.
app/api/webhooks/route.ts
// app/api/webhooks/route.ts
import { createWebhookHandler } from '@hookflo/tern/nextjs'
export const POST = createWebhookHandler({
platform: 'stripe',
secret: process.env.WEBHOOK_SECRET!,
queue: true, // reads QSTASH_* from env automatically
handler: async (payload) => {
// ✓ verified & queued — handle your event
return { received: true }
}
})
Environment variables
# required always
WEBHOOK_SECRET=whsec_xxx
 
# required with queue: true or explicit config
QSTASH_TOKEN=qstash_xxx
QSTASH_CURRENT_SIGNING_KEY=sig_xxx
QSTASH_NEXT_SIGNING_KEY=sig_xxx

Nothing gets lost.
Everything is replayable.

Events exhausting all retries land in your DLQ. Inspect them. Fix the root cause. Replay any time.

Dead Letter Queue3 failed
msg_26hZ4k...
payment_intent.failed
3/3 ✗
2m ago
msg_8xKp9r...
user.created
3/3 ✗
14m ago
msg_3rNz7m...
push
3/3 ✗
1h ago

Same event twice?
Processed once.

Tern resolves a stable deduplication ID per platform — Stripe idempotency key, GitHub delivery header, Clerk svix-id — with SHA-256 hash fallback. QStash drops duplicates within a 10-minute window.

evt_stripe_001
payment_intent.succeeded
processed
evt_stripe_001
payment_intent.succeeded
duplicate — dropped
evt_stripe_001
payment_intent.succeeded
duplicate — dropped
evt_stripe_002
payment_intent.succeeded
processed

Pay Upstash directly.
Not a middleman.

Tern is free and open source. You pay Upstash directly — no markup, no platform fee, no lock-in.

VolumeHookdeckTern + QStash
Base/month$39$0
500K events$44/mo~$5/mo
5M events$89/mo~$50/mo
Data throughTheir infraYour Upstash
Tern is free. You pay Upstash directly.
Upstash free tier
1,000 messages/day free
No credit card required
Upgrade only when you need to
Most indie projects = $0/month total

No event gets lost.
Every failure has a recovery path.

Open source · MIT licensed · Built at Hookflo