A built-in browser notifier: Web Push, with in-tab notifications as the fallback #209

Closed
opened 2026-09-15 18:47:37 +00:00 by tiagoagueda · 0 comments
Owner

A notifier that ships inside Postulo and reaches the person in their browser, so notifications work with no mail setup and no third-party plugin.

Today the only built-in notifier is Email, which needs working mail. Apprise (postulo-apprise) covers everything else, but it has to be installed and set up. A browser notification needs neither: the person allows it once in the browser they use Postulo in.

Two ways of delivering, both in one plugin

  1. Web Push (service worker + Push API). It arrives even when no Postulo tab is open.
    • Encrypted end to end (RFC 8291, aes128gcm) and signed with a VAPID key (RFC 8292).
    • The browser vendor's push service (Google, Mozilla, Apple) carries the message but can't read it. That service is the only third party involved, and the connection page says so.
  2. In-tab (the Notification API while a Postulo tab is open). This is the fallback when the browser can't subscribe (no push support, an instance reached over plain HTTP on a mesh VPN, or push refused) or when a push fails.
    • The message waits on the instance and the open tab collects it. Nothing leaves the instance.

Shape

  • Plugin postulo.plugins.browser:
    • kind notifier, name browser; its own package and its own locale/, like every other built-in (#126, #129);
    • one connection per browser;
    • its only field is the push subscription, stored as a secret and filled in by the page when the person presses Allow notifications in this browser;
    • with no subscription, the connection is in-tab only;
    • send() pushes when it can and otherwise leaves the notice for an open tab;
    • it never queries a model.
  • VAPID key: derived from POSTULO_FIELD_KEY / SECRET_KEY, the same material that encrypts connection secrets. Rotating it invalidates the subscriptions and their encryption together, which is already what happens to every other connection secret. There is no separate key to back up or lose.
  • Destination: the push endpoint comes from the browser, so it goes through the same guarded client as any connection (https only, private addresses refused unless the operator allows them).
  • The core provides what a plugin cannot:
    • /sw.js at the site root, served with the scope the service worker needs;
    • an owner-scoped endpoint an open tab asks for waiting notices;
    • the inbox those notices wait in;
    • the script half in app.js, since the CSP allows no other script source.
  • A generic hook: an optional plugin method form_attributes() hands values (the VAPID public key, the worker's address) to the connection page as data- attributes. No template checks the plugin's name.

Accessibility and honesty

  • Without scripts none of this can work, and the connection page says so instead of showing a button that does nothing.
  • Permission denied, push unsupported and in-tab only are each stated in words in a live region.
  • Test sends a real push when there is a subscription. With none, it says nothing could be proven from the server.

Done when

  • The plugin is registered and listed with the full manifest; test_plugin_manifests counts seventeen, and REACHING_PAST records why it reaches postulo.notifications.
  • Encryption matches the RFC 8291 worked example, and the VAPID token verifies against the public key.
  • Tests cover:
    • push, and refusal of a private or non-HTTPS endpoint;
    • a gone subscription (404/410) ending up in the in-tab inbox;
    • the inbox never showing one person's notices to another.
  • The wiki documents the notifier (Configuration → Notifications) and the new optional hook (Writing a plugin).
A notifier that ships inside Postulo and reaches the person in their browser, so notifications work with no mail setup and no third-party plugin. Today the only built-in notifier is **Email**, which needs working mail. Apprise (`postulo-apprise`) covers everything else, but it has to be installed and set up. A browser notification needs neither: the person allows it once in the browser they use Postulo in. ## Two ways of delivering, both in one plugin 1. **Web Push** (service worker + Push API). It arrives even when no Postulo tab is open. - Encrypted end to end (RFC 8291, `aes128gcm`) and signed with a VAPID key (RFC 8292). - The browser vendor's push service (Google, Mozilla, Apple) carries the message but can't read it. That service is the only third party involved, and the connection page says so. 2. **In-tab** (the Notification API while a Postulo tab is open). This is the fallback when the browser can't subscribe (no push support, an instance reached over plain HTTP on a mesh VPN, or push refused) or when a push fails. - The message waits on the instance and the open tab collects it. Nothing leaves the instance. ## Shape - **Plugin** `postulo.plugins.browser`: - kind `notifier`, name `browser`; its own package and its own `locale/`, like every other built-in (#126, #129); - one connection per browser; - its only field is the push subscription, stored as a secret and filled in by the page when the person presses *Allow notifications in this browser*; - with no subscription, the connection is in-tab only; - `send()` pushes when it can and otherwise leaves the notice for an open tab; - it never queries a model. - **VAPID key:** derived from `POSTULO_FIELD_KEY` / `SECRET_KEY`, the same material that encrypts connection secrets. Rotating it invalidates the subscriptions and their encryption together, which is already what happens to every other connection secret. There is no separate key to back up or lose. - **Destination:** the push endpoint comes from the browser, so it goes through the same guarded client as any connection (`https` only, private addresses refused unless the operator allows them). - **The core provides** what a plugin cannot: - `/sw.js` at the site root, served with the scope the service worker needs; - an owner-scoped endpoint an open tab asks for waiting notices; - the inbox those notices wait in; - the script half in `app.js`, since the CSP allows no other script source. - **A generic hook:** an optional plugin method `form_attributes()` hands values (the VAPID public key, the worker's address) to the connection page as `data-` attributes. No template checks the plugin's name. ## Accessibility and honesty - Without scripts none of this can work, and the connection page says so instead of showing a button that does nothing. - Permission denied, push unsupported and in-tab only are each stated in words in a live region. - *Test* sends a real push when there is a subscription. With none, it says nothing could be proven from the server. ## Done when - The plugin is registered and listed with the full manifest; `test_plugin_manifests` counts seventeen, and `REACHING_PAST` records why it reaches `postulo.notifications`. - Encryption matches the RFC 8291 worked example, and the VAPID token verifies against the public key. - Tests cover: - push, and refusal of a private or non-HTTPS endpoint; - a gone subscription (404/410) ending up in the in-tab inbox; - the inbox never showing one person's notices to another. - The wiki documents the notifier (*Configuration → Notifications*) and the new optional hook (*Writing a plugin*).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#209
No description provided.