Juan Fernando CastroandDaniel Salazar 8e1233d82a feat(push): notifications:push app permission (#4317)
* feat(push): notifications:push app permission

Apps need a per-app notifications:push grant for their notifications to
go out as pushes. hasPushConsent checks it as the app acting for the
user, so only that user's grant to that app answers yes, and a revoke
takes effect on the next check. The permission dialog describes it and
the perms docs list it.

* fix(push): canonical app uid in the push consent check

* fix(push): let users grant apps notifications:push

The grant route now only accepts permissions from a fixed set of
app-grantable families, which refused notifications:push, so an app could
never be given push consent. Register it next to events:background.

* refactor(permission): keep the notifications:push constant with the permission code

The consent check that reads it lives with the delivery code, outside
core. The grant flow test checks the grant through the permission
service directly.

* feat(notifications): node-local notif.written hook

NotificationService emits notif.written after each non-silent write and
after an in-place rewrite, on the node that wrote the row, so an
extension can act on new notifications without a second bus round trip.
It stays off the published event stream; notif.created carries the
notification itself.

* feat(puter-js): puter.notifications

An app notifies the user it runs for, reads its own notifications back
and follows new ones, hears clicks on them from the desktop, and asks
for or checks the notifications:push grant. Puter's own apps also
register, list and remove the user's devices.

* feat(puter-js): device calls report push_unavailable without a push service

A deployment with no push service has no /push routes, so core answers
the device calls with its catch-all 404. registerDevice, listDevices and
removeDevice turn that into push_unavailable, kept apart from a missing
device, which comes back as device_not_found.

* feat(homepage): serve the service worker at /sw.js

/sw.js serves the GUI's sw.js, preceded by one importScripts per script
an extension registered with registerServiceWorkerScript. Whether the
GUI registers the worker follows the pushEnabled gui param, which only
an extension sets. Notification reads also carry the row's app_uid, so
the GUI can route a tap to the app a notification is about.

* feat(gui): notifications settings for push devices and apps

Settings -> Notifications turns pushes on and off for this browser
through a push client an extension registers, lists the user's devices
and the apps allowed to push, and switches Puter's own pushes. The tab
shows only when the pushEnabled gui param is true. The sessions window
shares the user agent formatting with it.

* feat(gui): service worker that shows pushes and routes taps

sw.js shows each push as a notification and, on a tap, focuses or opens
a Puter window and hands it the tapped notification. The desktop routes
the tap to the app the notification names, opening it when needed, or
to the notifications panel. The worker is registered only when the
pushEnabled gui param is true.

* feat(gui): notifications settings read a missing push service as unavailable

Without a push extension the /push routes answer core's plain 404. The
GUI maps that to push_unavailable, so the tab says pushes aren't
available on this server instead of reporting a load failure, and a
device removal only counts as done when the device itself is missing.
The push client doc describes the payload sw.js reads and where the
push endpoints come from.

* fix(gui): keep the service worker on servers without push

Unregistering /sw.js wherever pushEnabled is unset ended the browser's
push subscription, so a single page load on such a server stopped pushes
from every server. Turning push off already ends the subscription.

* feat(gui): the service worker tells open tabs when the push subscription changes

* fix(gui): removing this browser in Settings also forgets its push registration

* fix(gui): removing this browser also deletes it on the server when its local record is gone

* fix(notifications): notif.written hands out a copy with the audience

Listeners get their own copy of the value, so what the socket carries is
what was written. Both emits name the audience, and an update names the
row's app when the row or the value says which.

* fix(notifications): read the app off the notification on an update

* fix(gui): route push taps through the stored notification

A tap from the worker names only a notification; the desktop and the
dashboard read the signed-in user's own row and route from its app,
target and data, as a tap from the URL already did. Without a row the
dashboard opens the list and the desktop does nothing.

* fix(gui): local banner tags carry the app

An app message's group is tagged under its app, as the server tags a
push, so two apps using the same group name keep separate banners.

* fix(gui): a tap delivered twice routes once

The page remembers the notifications it handed over in the last ten
seconds and drops a repeat, whichever way it arrives.

* fix(gui): the service worker never reuses cached imports

The worker is registered with updateViaCache set to none, so the scripts
it imports are fetched fresh on every update check.

* fix(gui): turn on can't run twice at once

The browser card's button stays out while the client is enabling or
disabling, through any redraw in between, and a status read begun before
the press no longer draws the card.

* feat(gui): turn on browser push when an app is allowed to notify

Allow on a permission request that includes notifications:push also
turns push on in this browser, from the same click, when it is off here.
The decision never depends on it. The permission popup starts the
service worker for such a request, answers the opener first and closes
once the registration is done. The Notifications tab records a browser
the user turned off, per user.

* feat(gui): register this browser for pushes again where it already allows them

On desktop and dashboard boot, and when an app that holds
notifications:push is launched, a browser whose notification permission
is already granted registers again without a click, unless the user
turned push off in this browser.

* feat(sdk): puter.notifications.enablePush() and the enable-push popup

An app on its own domain can offer to turn push on in the user's browser
from its own button: puter.notifications.enablePush() opens a popup on
the Puter origin whose single button supplies the click the browser's
prompt needs, and inside the desktop the same dialog is shown over IPC.
The popup hands the opener no token. Docs for the new call and for the
paths that now call the push client's enable().

* fix(gui): wait for the browser's prompt before closing the permission popup

The deadline on a registration begun from a click counts no time while
the browser's own notification prompt is open, in the permission popup
and in the enable-push dialog. The enable-push dialog keeps one
registration in flight, settles on whenever it resolves while open, and
offers a retry only after a real failure. A permission dialog that ends
in a denial after Allow began a registration turns push back off and
restores the opt-out it cleared.

* fix(gui): one enable-push dialog at a time, only for apps that may send pushes

The desktop and the popup show the enable-push dialog only when the
asking app holds notifications:push. The IPC branch answers a second
call unavailable while a dialog is up, as the feedback dialog does. The
silent registration on boot and app launch runs once at a time. A popup
whose opener cannot be attested answers the enable-push request before
it closes.

* feat(events): isOnline presence check for a user and app

* feat(notifications): onPush and enable with Web Push on the app's own origin

puter.notifications.onPush(key, handler) handles an app's notifications for
one channel: live over the events socket while the app is connected, and as
a browser push otherwise. enable() asks for notification permission on the
site's origin and subscribes through the site's worker, which imports
push-sw.js (served at js.puter.com/v2/push-sw.js). A new subscription is
confirmed by a challenge push the worker hands back to the page. disable()
removes it.

The GUI uses the same flow through /sw.js when the server publishes
pushWebPushKey.

* fix(notifications): visible sender for app messages; local banners follow push consent

An app title of only invisible or blank-filler characters no longer leaves
an app message without a sender; it falls back to the app's name, then its
uid. Hidden-tab banners now show only while push is on in this browser, and
for an app's message only when the app holds notifications:push.

* feat(notifications): enable() asks through Puter's prompt after a revoke

When the user turned an app's notifications off in Puter, the browser's
permission on the app's origin no longer re-enables them. enable() asks
again through Puter's permission prompt and retries once.

* fix(notifications): enable() rejects on a revoke, onPush() syncs quietly, one permission check per app a minute

enable() no longer opens Puter's prompt after a push_revoked error; apps call requestPermission() from a click and enable() again. onPush()'s page-load sync no longer reports push_unavailable or push_revoked. Hidden-tab banners for an app's messages check the app's push permission once a minute instead of once per message.

* docs(notifications): describe the confirmation step as what happens; one-line comments and neutral fixtures

* docs(notifications): guide for app push notifications, limits and prices

* fix(puter-js): subscribe once the worker is active, and retry a subscription the push service hasn't settled

* fix(gui): the desktop's hidden-tab banner is for Puter's own notifications only

* docs(notifications): app pushes at $2.50 per million

---------

Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
2026-10-29 09:19:09 -04:00
2026-10-21 13:04:23 -07:00
…
…
2026-10-21 13:04:23 -07:00
…

Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time.

The Open-Source Internet Computer!

« LIVE DEMO »

Puter.com · App Store · AI Builder · Developers · X

screenshot


Puter

Puter is an advanced, open-source, self-hostable internet computer designed to be feature-rich, fast, and highly extensible.

For Users

Puter's goal is to provide you with every app and feature you need to work, create, and play under one roof. From a simple Notepad and Voice Recorder to Spreadsheet and Camera, Puter wants to be the all-in-one solution for your digital life.

For Developers

Puter provides everything you need to build and publish web apps and games. From AI to Cloud Storage and Database to Serverless Workers, Puter has you covered. Puter also helps you get users! Once you build your app, you can publish it on our App Store to reach and monetize users.


Getting Started

💻 Local Development

git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start

→ This should launch Puter at http://puter.localhost:4100

To run this checkout with Docker, follow Building from source. Create a local docker-compose.override.yml to select the local build; keeping these settings out of docker-compose.yml avoids conflicts when pulling updates and keeps local configuration out of pull requests.


🚀 Self-Hosting

Linux/macOS

curl -fsSL https://puter.com/selfhost | sh

Windows

irm https://puter.com/selfhost?os=windows | iex

→ For more details, see Self-Hosting Puter.


☁️ Puter.com

Puter is available as a hosted service at puter.com.


Support

Connect with the maintainers and community through these channels:

We are always happy to help you with any questions you may have. Don't hesitate to ask!


License

This repository, including all its contents, sub-projects, modules, and components, is licensed under AGPL-3.0 unless explicitly stated otherwise. Third-party libraries included in this repository may be subject to their own licenses.


Translations

Languages
TypeScript 63.2%
JavaScript 33.7%
CSS 1.8%
HTML 1.2%