kukuroo
GitHub

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.
No server to keep alive.

A Worker and a KV namespace. Nothing to host, renew, or restart at 3am.

No company in the middle.

Your keys stay in your Worker, and the payload is encrypted to the device. Nobody's dashboard sees your alerts.

No app to build.

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 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":[]}

Anything that can make an HTTP call can ring your phone.

Install

Standalone is its own Worker at its own address, serving the enrolment page it ships with. Mounted is three lines inside a Worker you already run. You do not have to decide first: setup asks whether you want the front end it ships with, and that answer decides most of it. Find your line anyway:

I don't run a Cloudflare Worker yet.

Standalone →

I run one, and notification taps should land inside my own site.

Mounted →

I run one, but push can live on its own address, away from my site.

Standalone →

Standalone

Answer two questions

One command writes the Worker, generates every key, and installs them. Storage provisions itself. Pass the answers as flags instead and it asks nothing.

npx kukuroo init my-push Use the bundled front end? [Y/n] Require an invite code to enrol a device? [y/N] cd my-push && npx wrangler deploy
Put it on your Home Screen

Open the page in Safari, Add to Home Screen, open it from the icon. Apple's rule, once per device. The invite code, if you asked for one, is typed here.

POST when something happens

The curl above, from anything that can reach the internet. The response counts the devices it reached.

The first question is the personal one. With the code on, only devices you invite can enrol; with it off, anyone who reaches the page can, and will get everything you send. Both answers are one word in src/worker.ts afterwards, and no device re-enrols.

The full standalone setup, step by step →

Mounted into a Worker you already have

Hand it your requests first

handle returns null outside its prefix, so your own routing is untouched.

import { mountKukuroo } from "kukuroo"; const kukuroo = mountKukuroo({ prefix: "/push", requireInvite: true, }); export default { async fetch(request, env) { const hit = await kukuroo.handle(request, env); return hit ?? yourRouter(request, env); }, };
Bind KV, then generate the keys

The binding name is fixed; the namespace provisions on your next deploy.

// wrangler.jsonc "kv_namespaces": [{ "binding": "KUKUROO_SUBS" }] # then, from that directory npm install kukuroo && npx kukuroo init --mounted
Serve enrolment on your origin

Return the bundled page from a route of yours, or build your own against /push/subscribe. Devices enrol on your hostname, which is where a tap should land.

import { enrolmentPage } from "kukuroo"; return new Response(enrolmentPage({ subscribePath: "/push/subscribe", publicKeyPath: "/push/public-key", requireInvite: true, }), { headers: { "content-type": "text/html" } });

The full mounted setup, step by step →

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 enrolment 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.