Web push notifications
A push notification reaches a user whose page of the app is closed: “your video is ready”. The browser subscribes once, the app keeps its subscription, and when something happens the app posts an encrypted message to the browser’s push service (Google’s for Chrome, Apple’s for Safari, Mozilla’s for Firefox), which wakes the app’s service worker to show it. Sending is free: a subrequest from the Worker to the push service.
Set it up
ocre g pwa # the manifest and service worker that shows the messages
ocre g push
ocre migrate
create src/push.rs
create public/push.js
create tests/push.rs
create migrations/0002_create_push_subscriptions.sql
update templates/layout.html
update Cargo.toml
update .dev.vars
update src/lib.rs
src/push.rsanswersGET /push/key(the public VAPID key),POST /push/subscriptions(saves a browser’s subscription; the same browser again updates it) andPOST /push/subscriptions/delete, and sends withnotify_all(&ctx, &message), ornotify_user(&ctx, user_id, &message)when the app hasocre g auth(subscriptions then carry the signed-in user’s id).public/push.js, loaded by the layout, subscribes the browser when an element withdata-push-subscribeis clicked (browsers only ask for permission after a click):<button data-push-subscribe>Notify me</button>. It firespush:subscribed, orpush:error(whose default is an alert).- The table
push_subscriptionsholds each browser’s endpoint and keys. .dev.varsgets a VAPID key pair (VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY) andVAPID_SUBJECT, the contact push services may write to (mailto:orhttps:).Cargo.tomlturns on Ocre’spushfeature (P-256 for the encryption and the signatures).
Send
let message = ocre::push::message("Your video is ready", "Download it within 7 days.", "/videos/Xq3v9L");
let delivered = crate::push::notify_user(&ctx, user.id, &message).await?;
ocre::push::message(title, body, path) is the JSON the service worker of ocre g pwa shows; a click on the notification opens path. For each subscription, ocre::push::send encrypts the message for that browser (aes128gcm, RFC 8291), signs the request with the VAPID private key (RFC 8292, a 12-hour ES256 token) and posts it; the push service keeps it up to a day while the browser is offline. A subscription the push service answers 404 or 410 to is gone (the user revoked the permission or the browser dropped it): the generated code deletes it. One subrequest per browser, so notify_all sends to 40 at most per call; for more, send from a job, a batch per run. Messages are at most 4,079 bytes.
Browsers
Chrome, Edge and Firefox on desktop and Android accept subscriptions from any page served over HTTPS (or localhost). Safari on macOS does too; on iPhone and iPad (iOS 16.4 and later) only once the user has added the site to the Home Screen, which the manifest of ocre g pwa allows. The flow was checked in ocre dev against a local stand-in for a push service (the request’s headers and encrypted body, and a 410 deleting the subscription); the encryption matches the example of RFC 8291. Delivery through Google’s and Apple’s push services has not been run by Ocre’s tests (October 2026).
Production
ocre push-keys >> .prod.vars # a new key pair for production
ocre secrets push VAPID_PRIVATE_KEY --file .prod.vars
Then set VAPID_PUBLIC_KEY (the public half from .prod.vars) and VAPID_SUBJECT in worker.env of cloudflare.config.ts. Changing the key pair later invalidates every subscription: browsers must subscribe again.
See also
- Generators:
ocre g pushandocre g pwa. - Background jobs for sending to many subscribers.