Receiving a webhook is easy. Everything after it is the hard part.
Restaq checkpoints every step of your webhook handlers - retries resume past what already succeeded, historical events replay on demand, and every attempt leaves an audit trail.
npx restaq@latest initNothing to install first - scaffolds relay.ts and installs exactly what you choose.
Works with the stack you already have
Durable steps
ctx.step.run(name, fn) checkpoints every unit of work. A retry resumes past whatever already succeeded - it never re-runs a completed step's side effects.
Automatic retries
A failed execution schedules its own retry with exponential backoff, up to a configurable cap - no queue infrastructure, no polling.
Replay & restart
Resume in place, force every step to re-run, or reprocess a historical event as a brand-new execution - three distinct recovery tools, not one overloaded button.
Real signature verification
Stripe, GitHub, Clerk, Shopify, and Resend plugins all do actual HMAC verification against the raw request body - not a stub.
Storage, auto-detected
Pass a pg.Pool, better-sqlite3 database, or mysql2 pool straight into restaq({ database }) - the dialect is detected automatically, migrations included.
Fully-typed events
relay.on('stripe.charge.succeeded', ...) autocompletes every event your registered plugins can emit, with event.data typed per event.
Webhooks in, durable executions out
Every provider delivery lands in the same durable pipeline before it ever reaches your code.
A handler is just TypeScript
No workflow DSL to learn - relay.on() registers a plain async function, and ctx.step.run() is the only new concept.
relay.on('stripe.charge.succeeded', async (event, ctx) => {
const charge = event.data.object; // typed as Stripe.Charge
const payment = await ctx.step.run('charge-payment', async () => {
return { orderId: charge.metadata.orderId, amount: charge.amount };
});
await ctx.step.run('send-confirmation', async () => {
ctx.log.info('order confirmed', payment);
});
});