npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xtandard/webhooks

v0.1.2

Published

Self-hosted, embeddable, Standard Webhooks-compliant outbound webhook control plane with pluggable storage, an in-process delivery engine, and an embeddable consumer portal.

Readme

@xtandard/webhooks

Self-hosted, embeddable, Standard Webhooks-compliant outbound-webhooks control plane.

Mount it inside your existing app, point it at the database you already run, and ship signed events with retries, dead-lettering, and a customer-facing portal — no per-message SaaS pricing, no separate service to operate.

CI npm license

publish() never blocks on a customer's server. Your app publishes; the dispatcher inside your process (or a dedicated worker running the same library) delivers; your customers self-serve through an embeddable portal.

Contents

Why another webhooks tool?

Webhook delivery is a library concern, not a service you rent or a second deployment you babysit.

  • Svix is excellent — and a SaaS (or a heavyweight self-hosted server with its own Postgres/Redis to operate). This is bun add + the DB you already have.
  • Hookdeck / Convoy are services to operate. Same objection.
  • Hand-rolled senders reimplement signing, retries, and endpoint management — usually badly, always repeatedly. This packages the 20% everyone needs: applications, event types, endpoints, signed delivery, exponential retries, dead-letters, replay, observability, and a portal.

And because the wire contract is Standard Webhooks, your receivers verify with the official standardwebhooks libraries in Python, Go, Ruby, Java, Rust, PHP, or the zero-dependency @xtandard/webhooks/receiver — nothing bespoke on their side.

Sibling project: @xtandard/flags — same architecture, same design system, for feature flags.

How it works — the two planes

flowchart LR
  subgraph host["your app process"]
    A[app code] -- "publish()" --> C[core]
    UI[admin UI / portal] --> C
    D[dispatcher] --> C
  end
  C <--> S[(your database)]
  D -- "signed POSTs, retries" --> E1[customer endpoint]
  D --> E2[customer endpoint]
  • Control plane — CRUD on applications, event types, endpoints; browsing messages/deliveries; replay. Hook-guarded, audited, auth'd.
  • Delivery planepublish() is one message write + fan-out (no HTTP, never throws because an endpoint is down). The in-process dispatcher owns all network I/O and retries; leases make it crash-safe and multi-instance-safe. At-least-once semantics; receivers dedupe on webhook-id.

Kill the process mid-retry-schedule and restart it: pending deliveries resume. The admin UI can be completely unmounted and delivery still works.

Quickstart

docker run -p 3000:3000 -e STORAGE_DRIVER=memory ghcr.io/xantiagoma/xtandard-webhooks
# open http://localhost:3000
bunx @xtandard/webhooks serve   # STORAGE_DRIVER=file by default → ./.webhooks
import { Elysia } from "elysia";
import { webhooksPanel } from "@xtandard/webhooks/elysia";
import { createMemoryStorage } from "@xtandard/webhooks/storage/memory";

const webhooks = webhooksPanel({ storage: createMemoryStorage() });
new Elysia().mount("/webhooks", webhooks.fetch).listen(3000);
import { Hono } from "hono";
import { webhooksPanel } from "@xtandard/webhooks/hono";
import { createMemoryStorage } from "@xtandard/webhooks/storage/memory";

const app = new Hono();
app.route("/webhooks", webhooksPanel({ storage: createMemoryStorage() }));
import express from "express";
import { webhooksPanel } from "@xtandard/webhooks/express";
import { createMemoryStorage } from "@xtandard/webhooks/storage/memory";

const app = express();
app.use("/webhooks", webhooksPanel({ storage: createMemoryStorage() })); // before body-parser
app.listen(3000);
import { webhooksPanel } from "@xtandard/webhooks/bun";
import { createMemoryStorage } from "@xtandard/webhooks/storage/memory";

const webhooks = webhooksPanel({ storage: createMemoryStorage(), basePath: "/webhooks" });
Bun.serve({ port: 3000, fetch: webhooks.fetch });

Mounting the panel starts an in-process dispatcher by default (dispatcher: false for split-worker deployments).

Send your first webhook

const { core } = webhooks;

await core.createApplication({ key: "acme" }); // your customer
await core.upsertEventType({ name: "invoice.paid" }); // the catalog
await core.createEndpoint("acme", {
  url: "https://api.acme-customer.com/webhooks", // their receiver
  eventTypes: ["invoice.paid"],
});

// The hot path — in the handler where the thing actually happens:
await core.publish("acme", {
  eventType: "invoice.paid",
  payload: { invoiceId: "inv_123", amount: 4200 },
  idempotencyKey: `invoice-paid-inv_123`, // safe to call twice
});

Verify on the receiving side

TypeScript (this package, zero deps, any WinterCG runtime):

import { verifyWebhook } from "@xtandard/webhooks/receiver";

export default async function handler(request: Request) {
  const event = await verifyWebhook(request, process.env.WEBHOOK_SECRET!); // throws if invalid
  // event.type === "invoice.paid", event.data === { invoiceId, amount }
  return new Response("ok");
}

Python / Go / anything — the official Standard Webhooks libraries verify deliveries from this package unmodified:

from standardwebhooks.webhooks import Webhook
payload = Webhook(secret).verify(body, headers)  # raises on failure

examples/receivers/ runs FastAPI + Go receivers against a live dispatcher as the interop proof. Details: docs/SIGNING.md.

The delivery model

attempt:   #1      #2      #3      #4       #5      #6      #7
delay:     0s ───► 5s ───► 5m ───► 30m ───► 2h ───► 5h ───► 10h ──► dead-letter
                         (±10% jitter; fully configurable)
  • Failures walk the schedule; exhaustion dead-letters (never silently drops) — visible in the UI with per-attempt HTTP detail, replayable one-at-a-time or in bulk (recover an endpoint since a timestamp).
  • Endpoints failing every attempt for 5 consecutive days auto-disable (configurable); disabled endpoints hold deliveries and resume on enable.
  • Every attempt hits the fire-and-forget onDelivery sink (metrics); terminal transitions fire after hooks (delivery.succeeded / delivery.exhausted — the offload point).
  • Secret rotation keeps the old secret verifying for a 24h grace window, with both signatures in the header.

Details: docs/DELIVERY.md.

The consumer portal

The Svix "App Portal" experience, self-hosted: your customers manage their own endpoints and inspect their own deliveries inside your product, scoped by a signed token — no sessions, no proxy routes.

// Your server: mint a token after your own auth
const token = await createPortalToken(process.env.PORTAL_SECRET!, customer.appKey);

// Your React frontend:
import { WebhooksPortal } from "@xtandard/webhooks/react";
import "@xtandard/webhooks/react/styles.css";

<WebhooksPortal baseUrl="/webhooks" token={token} />;

Cross-application access is denied by construction — the host's authorization is never consulted for portal principals. Details: docs/PORTAL.md.

Storage backends

Point it at what you already run — the whole system sits on a four-method KV contract (ADR 0005):

| Backend | Subpath | Notes | | -------------- | ----------------------------------------------- | --------------------------------------------------------------------------- | | Memory | storage/memory | dev/tests/demo; every capability | | File | storage/file | zero-dep persistence | | Redis | storage/redis | native queue claiming (sorted set + Lua), pub/sub watch, JSON codec variant | | Postgres | storage/postgres | jsonb KV, lazy DDL | | Drizzle | storage/drizzle + drizzle/{pg,mysql,sqlite} | your ORM, your migrations | | MongoDB | storage/mongodb | | | SQLite (Bun) | storage/sqlite | bun:sqlite | | libSQL / Turso | storage/libsql | | | unstorage | storage/unstorage | 20+ drivers via unjs | | Cloudflare KV | storage/cloudflare-kv | dispatch via cron trigger |

Split planes: control data in Postgres, delivery queue in Redis — queueStorage. Details: docs/STORAGE.md.

Subpath exports

| Import | What you get | | ---------------------------------------- | --------------------------------------------------------------------------- | | @xtandard/webhooks | core, dispatcher, panel handler, signing, portal tokens, hooks contract | | …/receiver | verifyWebhook — zero-dep verification of any Standard Webhooks sender | | …/signing | low-level sign/verify primitives | | …/schema | types only | | …/testing | test core + verifying local receiver + drain helper | | …/storage/*, …/drizzle/* | storage adapters (optional peers) | | …/auth/{none,basic,delegated} | authentication providers | | …/authorization/{none,roles,delegated} | authorization providers | | …/hooks/log | reference logging hook | | …/{elysia,hono,express,bun} | framework adapters | | …/react, …/react/styles.css | <WebhooksDashboard> + <WebhooksPortal> embeds |

Examples

bun run demo boots a seeded playground on http://localhost:7789 — two applications, a grouped event catalog, healthy/flaky/dead endpoints producing real live attempt history, dead-letters to replay.

| Example | Shows | | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | elysia / hono / express | panel + publish-on-user-action loop per framework | | full-loop | sender + verifying receiver in one command; watch retries live | | portal-embed | <WebhooksPortal> inside a host React app | | auth | none/basic/delegated + roles + portal side-by-side | | receivers | polyglot proof: official Python + Go libs verifying our deliveries | | storage-drivers | one contract, every backend | | postgres-redis | split planes: control in Postgres, queue in Redis | | split-worker | web publishes, worker dispatches | | standalone-docker | compose file for the image |

Screenshots

| | | | ------------------------------------- | --------------------------------------- | | Overview | Endpoints |

CLI

xtandard-webhooks serve            # panel + dispatcher from env
xtandard-webhooks dispatch         # dispatcher only (split worker)
xtandard-webhooks init             # create an app + example event type
xtandard-webhooks list-apps
xtandard-webhooks list-endpoints --app acme
xtandard-webhooks publish --app acme --type invoice.paid --data '{"n":1}'
xtandard-webhooks retry --app acme --delivery dlv_…
xtandard-webhooks verify --secret whsec_… --payload-file body.json --headers-file headers.json
xtandard-webhooks listen --port 4000 --secret whsec_…   # local inspecting receiver
xtandard-webhooks sign --secret whsec_… --data '{"n":1}' --url http://localhost:4000

Env contract (storage drivers, auth, retention, RETRY_SCHEDULE, …): docs/DEPLOYMENT.md.

Testing webhooks

Self-hosted equivalents of webhook.site / Svix Play / the Standard Webhooks simulator — no hosting, no accounts:

  • xtandard-webhooks listen — a local inspecting receiver: point an endpoint at http://localhost:4000 and every incoming webhook is pretty-printed; with --secret it verifies the signature (VERIFIED/FAILED) and answers 401 on failure so senders exercise their retry path.
  • xtandard-webhooks sign — the signature playground: builds a fully signed request from a secret + payload, printing the headers/body and a ready-to-run curl (with --url).
  • Panel "Request" inspector — on any delivery's detail, see the exact signed request it sends (method, URL, all headers including webhook-signature, body) with a Copy-as-curl button.
  • @xtandard/webhooks/testing — programmatic: createTestReceiver (a verifying local receiver) + drainDeliveries for deterministic tests.

See docs/TESTING.md.

Documentation

Getting started · Architecture · Delivery · Signing · Storage · Portal · Auth · Authorization · Hooks · UI · Adapters · Deployment · Testing · Releases · ADRs

Project status

ZeroVer (0.x) and pre-first-publish: the surface is stabilizing, minor versions may break, and feedback shapes it — issues welcome. Anti-bloat is a core value: when a feature is composable from an existing primitive in ten lines, we document the recipe instead of shipping the subsystem.

License

MIT