* 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>
The Open-Source Internet Computer!
« LIVE DEMO »
Puter.com
·
App Store
·
AI Builder
·
Developers
·
X
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:
- Bug report or feature request? Please open an issue.
- X (Twitter): x.com/HeyPuter
- Security issues or abuse reports? security@puter.com
- Email maintainers at hi@puter.com
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
- Arabic / العربية
- Armenian / Հայերեն
- Bengali / বাংলা
- Chinese / 中文
- Danish / Dansk
- English
- Farsi / فارسی
- Finnish / Suomi
- French / Français
- German / Deutsch
- Hebrew/ עברית
- Hindi / हिंदी
- Hungarian / Magyar
- Indonesian / Bahasa Indonesia
- Italian / Italiano
- Japanese / 日本語
- Korean / 한국어
- Malay / Bahasa Malaysia
- Malayalam / മലയാളം
- Dutch / Nederlands
- Polish / Polski
- Odia / ଓଡ଼ିଆ
- Portuguese / Português
- Punjabi / ਪੰਜਾਬੀ
- Romanian / Română
- Russian / Русский
- Spanish / Español
- Swedish / Svenska
- Tamil / தமிழ்
- Telugu / తెలుగు
- Thai / ไทย
- Turkish / Türkçe
- Ukrainian / Українська
- Urdu / اردو
- Vietnamese / Tiếng Việt

