Reliable webhook processing primitives for TypeScript.
SafeHook helps you verify, dedupe, track, and replay webhook deliveries without forcing you into a framework, queue, ORM, or storage stack.
Webhook handlers usually need the same reliability layer:
- verify provider signatures against the exact raw request body
- prevent duplicate execution during retries and redeliveries
- coordinate concurrent deliveries safely
- persist processing state for debugging and replay
- expose clean hooks for metrics and operations
SafeHook focuses on that layer and stays out of the rest of your application.
SafeHook is:
- framework-agnostic
- storage-agnostic
- dependency-light
- built for exact raw-body verification
- designed for idempotent, replay-safe processing
SafeHook is not:
- a workflow engine
- a queue system
- an event bus
- a webhook SaaS
- an orchestration platform
- a backend framework
npm install @safehook/safehookSafeHook does not install Redis, PostgreSQL, or provider SDKs for you. If your application wants those integrations, your application owns those dependencies.
import { createSafeHook, memoryStore, stripe } from "@safehook/safehook";
const safehook = createSafeHook({
store: memoryStore(),
});
await safehook.process({
rawBody,
headers,
provider: stripe({
secret: process.env.STRIPE_WEBHOOK_SECRET!,
}),
onEvent: async (event, ctx) => {
console.log("processed", ctx.eventType, ctx.idempotencyKey);
},
});You bring:
- the exact
rawBody - request
headers - provider configuration
- storage choice
- business logic
SafeHook provides:
- signature verification
- provider event parsing
- idempotency key resolution
- atomic duplicate prevention
- processing state tracking
- replay-safe execution
- lifecycle hooks for observability
createSafeHook()for reusable process/replay orchestrationprocessWebhook()andreplayWebhook()core functions- built-in stores:
memoryStore()redisStore(client, { mode: "node-redis" })postgresStore(client, { mode: "pg" })
- built-in providers:
stripe()github()customProvider()
- HTTP helper:
handleWebhookHttp()
- framework adapter helpers:
- Express
- Fastify
- Hono
- Next route handler style
- metrics helpers:
- OpenTelemetry
- Prometheus
SafeHook defaults are intentionally conservative:
- exact
rawBodyis always required for verification and parsing eventPayloadis stored by default so replay can workrawBodyis not stored unlessstoreRawBody: true- normalized
headersare not stored unlessstoreHeaders: true
SafeHook verifies and parses against the rawBody you provide. It does not reserialize or mutate the body before signature verification.
If you want replay support without persisting rawBody or normalized headers, start here:
handleWebhookHttp() maps common SafeHook failures into deterministic HTTP responses:
- invalid signature ->
401 - parse or metadata problems ->
400 - store or infrastructure failures ->
500 - replay/conflict-style failures ->
409
This keeps invalid webhook traffic from surfacing as generic framework 500 errors.
SafeHook does not own infrastructure.
You can use:
- in-memory storage for local development and tests
- Redis for distributed claim coordination
- PostgreSQL for durable audit trails and failure review
- custom stores that implement the SafeHook store contract
Mode-specific typing is exported when you want stronger TypeScript support for concrete client shapes:
NodeRedisClientNodeRedisStoreOptionsPgClientPgStoreOptions
See docs/stores.md.
SafeHook keeps provider payloads broad by default so upstream event changes do not get excluded by narrow package types.
For stronger typing on common payload families, SafeHook also exports helper types.
Stripe examples:
StripeCheckoutSessionEventStripeInvoiceEventStripePaymentIntentEvent
GitHub examples:
GitHubIssuesEventGitHubPullRequestEventGitHubPushEvent
Both stripe() and github() support generic typing when you want to narrow payloads intentionally.
See docs/providers.md.
SafeHook stores lifecycle state so operators can inspect failures and replay events safely.
Tracked statuses include:
receivedprocessingsucceededfailedduplicateexpired
Replay is handler-driven: SafeHook manages the reliability flow, and your application supplies the business handler.
See:
SafeHook is a focused reliability layer for incoming webhooks. That focus is deliberate. The package should stay small, predictable, and easy to compose into different application architectures.