Hono walkthrough
better-push on a Hono API, with the emulator standing in for a client.
See it running and inspect apps/demos/hono.
A Hono API server, from nothing to a notification you can see. Hono serves an API rather than a browser, so there is no service worker and no permission prompt - the devices are whatever clients you serve, and the emulator stands in for one while you build.
You need a Hono project on Node with a DATABASE_URL.
1. Scaffold
npx @better-push/cli init --framework honoDetected Hono · node-postgres (pg) · package manager pnpm · source root src
Planned changes
create src/push.ts
create src/push-route.ts
create better-push.sql
env .env.local (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT, …)
note No service worker was written: this framework serves an API, not a
browser. Register devices by POSTing to /api/push/devices from whatever
client you serve.2. Install and mount
pnpm add @better-push/core pgimport { Hono } from "hono";
import { serve } from "@hono/node-server";
import { pushRoutes } from "./push-route";
const app = new Hono();
app.route("/", pushRoutes);
serve({ fetch: app.fetch, port: 3000 });Do not compress the event stream
GET /api/push/events is SSE. A compression middleware buffers it, turning a
live feed into one long silence followed by everything at once. Exclude that
path, or mount compression after pushRoutes.
3. Create the tables
npx @better-push/cli migrate4. Wire your session
Hono has no session convention, so this is yours to write. The resolver gets the
raw Request:
session: async (request) => {
const token = request.headers.get("authorization")?.replace("Bearer ", "");
const user = token ? await verify(token) : null;
return user ? { userId: user.id } : null;
},5. Check it
npx @better-push/cli doctorIt knows this is Hono, finds src/push-route.ts, and does not complain
about a missing service worker - an API server has no page to register one from.
6. See a notification
import { emulator } from "@better-push/core/providers/emulator";
providers: [
webPush({ vapid: { /* ... */ } }),
...(process.env.NODE_ENV === "production" ? [] : [emulator()]),
],npx @better-push/cli devThere is no browser here, so register the virtual device with a request - which is exactly what your real clients will do:
curl -X POST http://localhost:3000/api/push/devices \
-H "authorization: Bearer $YOUR_TOKEN" \
-H "content-type: application/json" \
-d '{"platform":"ios","provider":"emulator","token":"emulator-dev-1"}'Then send:
await push.notify({ userId: "alice", title: "Hello", body: "It works." });It appears at http://127.0.0.1:4984, tagged ios because that is what the
device registered as.
7. The real clients
Hono usually fronts a mobile app or a separate frontend. Both register the same way:
- React Native:
@better-push/core/nativehandles the permission flow, the token, and re-registration on rotation. - A separate web frontend:
usePushRegistrationfrom@better-push/core/react, withbaseURLpointing at this API. Servesw.jsfrom the frontend's origin, not this one - a service worker's scope is its own origin.