@better-webhook/hono
v0.3.4
Published
Hono adapter for better-webhook
Maintainers
Readme
@better-webhook/hono
Hono adapter for type-safe webhooks.
Drop-in Hono handler that handles signature verification, payload parsing, and type-safe event routing.
import { Hono } from "hono";
import { github } from "@better-webhook/github";
import { push } from "@better-webhook/github/events";
import { toHono } from "@better-webhook/hono";
const app = new Hono();
const webhook = github().event(push, async (payload) => {
console.log(`Push to ${payload.repository.name}`);
});
app.post("/webhooks/github", toHono(webhook));
export default app;For a runnable local app, see apps/examples/hono-example.
Features
- 🔌 Drop-in handler — Works with any Hono runtime
- 🔒 Automatic verification — Signatures verified before your handler runs
- 📝 Type safe — Full TypeScript support
- 🌍 Runtime-agnostic — Works on Node, Workers, Bun, and Deno
Installation
npm install @better-webhook/hono
# or
pnpm add @better-webhook/hono
# or
yarn add @better-webhook/honoQuick Start
1. Install a provider
npm install @better-webhook/github2. Create your Hono app
import { Hono } from "hono";
import { github } from "@better-webhook/github";
import { push, pull_request } from "@better-webhook/github/events";
import { toHono } from "@better-webhook/hono";
const app = new Hono();
const webhook = github()
.event(push, async (payload) => {
const branch = payload.ref.replace("refs/heads/", "");
console.log(`Push to ${branch} by ${payload.pusher.name}`);
})
.event(pull_request, async (payload) => {
if (payload.action === "opened") {
await notifySlack(`New PR: ${payload.pull_request.title}`);
}
});
app.post("/webhooks/github", toHono(webhook));
export default app;3. Set your secret
export GITHUB_WEBHOOK_SECRET=your-secret-hereNode.js
Use @hono/node-server and the toHonoNode helper:
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { github } from "@better-webhook/github";
import { push } from "@better-webhook/github/events";
import { toHonoNode } from "@better-webhook/hono";
const app = new Hono();
const webhook = github().event(push, async (payload) => {
console.log(`Push to ${payload.repository.name}`);
});
app.post("/webhooks/github", toHonoNode(webhook));
serve(app);Cloudflare Workers
import { Hono } from "hono";
import { github } from "@better-webhook/github";
import { push } from "@better-webhook/github/events";
import { toHono } from "@better-webhook/hono";
const app = new Hono();
const webhook = github().event(push, async (payload) => {
console.log(`Push to ${payload.repository.name}`);
});
app.post("/webhooks/github", toHono(webhook));
export default app;Bun
import { Hono } from "hono";
import { github } from "@better-webhook/github";
import { push } from "@better-webhook/github/events";
import { toHono } from "@better-webhook/hono";
const app = new Hono();
const webhook = github().event(push, async (payload) => {
console.log(`Push to ${payload.repository.name}`);
});
app.post("/webhooks/github", toHono(webhook));
export default {
port: 3000,
fetch: app.fetch,
};Deno
import { Hono } from "npm:hono";
import { github } from "npm:@better-webhook/github";
import { push } from "npm:@better-webhook/github/events";
import { toHono } from "npm:@better-webhook/hono";
const app = new Hono();
const webhook = github().event(push, async (payload) => {
console.log(`Push to ${payload.repository.name}`);
});
app.post("/webhooks/github", toHono(webhook));
Deno.serve(app.fetch);If you prefer bare specifiers ("hono", "@better-webhook/github", etc.),
add mappings in deno.json:
{
"imports": {
"hono": "npm:hono",
"@better-webhook/github": "npm:@better-webhook/github",
"@better-webhook/github/events": "npm:@better-webhook/github/events",
"@better-webhook/hono": "npm:@better-webhook/hono"
}
}Raw Body Notes
Webhook signature verification depends on the raw request body. Avoid consuming
c.req.raw directly before the adapter runs. If you need the body in middleware,
use HonoRequest methods like c.req.text() or c.req.arrayBuffer() so the
adapter can reconstruct the raw payload when needed.
Configuration Options
Custom Secret
app.post(
"/webhooks/github",
toHono(webhook, {
secret: process.env.MY_GITHUB_SECRET,
}),
);Success Callback
app.post(
"/webhooks/github",
toHono(webhook, {
onSuccess: async (eventType) => {
metrics.increment("webhook.success", { event: eventType });
},
}),
);Body Size Guard
app.post(
"/webhooks/github",
toHono(webhook, {
maxBodyBytes: 1024 * 1024, // 1MB
}),
);Use maxBodyBytes as an app-layer guard. Keep edge/proxy limits configured for
early rejection before the request body is fully buffered.
Observer
import { createWebhookStats } from "@better-webhook/core";
const stats = createWebhookStats();
app.post(
"/webhooks/github",
toHono(webhook, {
observer: stats.observer,
}),
);Response Codes
| Code | Meaning |
| ----- | ----------------------------------------------------------------- |
| 200 | Webhook processed successfully |
| 204 | No handler registered for this event type (after verification) |
| 409 | Duplicate replay key detected (when replay protection is enabled) |
| 400 | Invalid body or schema validation failed |
| 401 | Signature verification failed |
| 405 | Request method is not POST |
| 413 | Request body exceeds maxBodyBytes |
| 500 | Handler threw an error |
Note: with app.post(...), non-POST requests may return 404 at the routing
layer before the adapter runs. 405 is returned when the adapter itself
receives a non-POST request (for example, via app.all(...)).
License
MIT
