Self-hosted web push worker,
works on iOS.
A push endpoint you own, in one Cloudflare Worker.
- Notifications you have to act on should not be buried in a Telegram channel, a Discord server, or email.
- The other way onto a lock screen is shipping a native app whose only job is that one line.
A Worker and a KV namespace. Nothing to host, renew, or restart at 3am.
Your keys stay in your Worker, and the payload is encrypted to the device. Nobody's dashboard sees your alerts.
iOS has rendered these natively since 18.4. No App Store, no $99 a year for four words on a lock screen.
Send a push notification in seconds
From cron, CI, a Raspberry Pi, your backend, a shell alias. Anything that can make an HTTP call can ring your phone.
$ curl -X POST https://push.example.com/push/send \ -H "authorization: Bearer $KUKUROO_SEND_TOKEN" \ -d '{"notification":{"title":"Task W13 finished", "body":"W14 is unblocked and ready to start", "navigate":"https://push.example.com/"}}' {"delivered":1,"removed":0,"failures":[]}
One-Click Integration
Setup takes less than a minute. One command writes the Worker, generates every key, installs them, provisions storage, and deploys.
$ npx kukuroo init my-push
That's it. You now have a push server of your own. Open the printed URL in Safari on your iPhone, Add to Home Screen, and open it from the icon. Apple's rule, once per device.
Already have a Worker? Mount it.
If you already run a site on Cloudflare Workers, Kukuroo mounts into the router you have. Devices enroll on your hostname, which is where a notification tap should land.
handle returns null outside its prefix, so your own
routing is untouched.
import { mountKukuroo } from "kukuroo";
const kukuroo = mountKukuroo({ prefix: "/push" });
export default {
async fetch(request, env) {
const hit = await kukuroo.handle(request, env);
return hit ?? yourRouter(request, env);
},
};
The binding name is fixed; the namespace provisions on your next deploy.
"kv_namespaces": [{ "binding": "KUKUROO_SUBS" }] # then, from that directory $ npm install kukuroo && npx kukuroo init --mounted
Return the bundled page from a route of yours, or build your own
against /push/subscribe.
import { enrollmentPage } from "kukuroo";
return new Response(enrollmentPage({
subscribePath: "/push/subscribe",
publicKeyPath: "/push/public-key",
}), { headers: { "content-type": "text/html" } });
Prerequisites
Kukuroo supports a short list, on purpose:
- Cloudflare Workers. The only place it runs. No Docker image, no binary, no other host.
- Safari. Declarative Web Push has shipped nowhere else yet. Other browsers are turned away at the enrollment page rather than left half working.
- iOS 18.4 or later, with the page on your Home Screen and opened from the icon. A Safari tab will not do, and that is Apple's rule rather than ours. macOS Safari 18.5 receives too, if you can think of a reason to want it.
- About 50 devices per send if you are on a free-plan Worker. That is the subrequest ceiling, and about 47 more phones than one person owns.