Notify - Web Push on Punk
The example application for
Punk::Push. Generated with
punk new Notify and then given push notifications.
perl bin/seed.pl # a database, one user, and a VAPID keypair
set -a; . var/vapid.env; set +a
plackup app.psgi
prove -l t/
Then open http://localhost:5000/, sign in as demo@example.com / demo,
and press Enable notifications.
app.psgi and t/01-basic.t add the distribution's blib to @INC so the
demo runs before Punk::Push is installed. Drop those lines once it is.
What to look at
The keys are configuration. bin/seed.pl writes a pair to
var/vapid.env once, and the application reads them from the environment. It
does not generate them at boot, because Hyperman runs a pool: a key minted per
process would differ per worker and per restart, and every subscription made
against the old one would be undeliverable in silence. punk push keys prints
a pair for a real deployment.
Subscribing needs a user. A subscription belongs to somebody, so
/push/subscribe is guarded - an unauthenticated POST that writes one lets
anybody who can guess a user id register their own browser for that user's
notifications. That is why this demo carries a sign-in at all. /push/key is
not guarded; it is a public key.
The model is inherited, not copied. lib/Notify/Model/PushSubscription.pm
is four lines:
package Notify::Model::PushSubscription;
use parent 'Punk::Model::PushSubscription';
It inherits the table and all nine columns. It exists only so the model lives
in this application's namespace, where somebody would look for it. Delete it
and the plugin registers its own. To add a column, add use Punk::Model; and
declare it - the inherited ones stay, and a redeclared field keeps its
position so the column order does not shift. (Needs Punk 0.45.)
Scope. The worker is served from /push/ but must control the whole
site, so the plugin sends Service-Worker-Allowed: / with it. Without that
header the browser refuses to register it at all.
Why nothing happens, in order. Push needs a secure context: localhost
counts, other plain-HTTP hosts do not. Permission must be requested from a
click, and once denied cannot be re-requested. A service worker must be
registered and activated - the application tab in devtools shows it.
The send form carries the whole surface. Everything a payload may hold -
title, body, url, icon, badge, tag - and everything one send may
override: ttl, urgency, topic. Blank fields are left out rather than
sent empty, since an icon of "" is a broken image and a topic of ""
would collapse every notification into one. The icon and badge it offers are
shipped in root/static/, so they resolve.
Sending. /notify calls $c->push_send($user_id, {...}), which fans out
over every subscription that user has and returns a result each. One dead
device does not stop the others, and the page prints every outcome including
the failures.
A 410 means gone. If a push service says a subscription is 404 or 410
the row is deleted; anything else, a 5xx included, leaves it alone.
Subscribe from a browser, then clear the site's data in that browser, then
send: the next send prunes the row.