-
Notifications
You must be signed in to change notification settings - Fork 41
pwa
Stable.
OpenStation ships a web app manifest, a service worker, and a local notifications API so users can install their WordPress site as a real OS app and plugins can surface alerts the same way native apps do.
This page is the architectural ground-truth. For the plugin-author's
copy-paste recipes see
docs/examples/pwa-install.md and
docs/examples/notify.md.
| Surface | Behaviour |
|---|---|
| Web app manifest | Served at /openstation/manifest.webmanifest. Site name + short name, theme color, icons (Site Icon when set, plugin logo otherwise), start_url=/wp-admin/index.php?desktop_mode_portal=1, scope=/wp-admin/ (narrowed from / so front-end links escape the PWA window; the manifest id stays at /openstation/ so existing installs aren't reset). Filterable via openstation_pwa_manifest. |
| Service worker | Served at /openstation/sw.js with Service-Worker-Allowed: /. Registered at root scope with a deliberately narrow fetch handler — it only intercepts paths under /openstation/ and /wp-admin/, plus the plugin's own static assets. wp-admin HTML is always network-first (nonces would otherwise drift). |
| Install hint | A system tile on the dock (id: 'os-pwa-install') registered on shell boot — except when the shell is already running standalone. It is removed live when display-mode flips to standalone or when getInstalledRelatedApps() reports the app installed (Chromium); on Safari / Firefox it persists as a fallback. Clicking it dispatches the browser install prompt when the site is currently installable, otherwise shows a contextual toast ("already installed", "not yet"). |
| Local notifications |
wp.os.notify({ title, body, icon, tag, onClick }) — uses the browser Notification API, falls back to a toast when permission is denied or the browser doesn't support it. |
| Push notifications |
Not in v1. The SW registers a no-op push handler (claimed so future v2 push payloads aren't silently dropped); the notificationclick handler is live — it closes the notification, focuses an existing /openstation/ window client, or opens notification.data.url (default /openstation/) when none exists. The same wp.os.notify shape will route through the SW's showNotification once push is wired. |
A service worker has exactly one scope path. The only common ancestor of
/openstation/ and /wp-admin/ is /. Registering at /openstation/
would cut the SW off from admin-page navigations — defeating the purpose
for the typical install target (a dashboard URL inside wp-admin).
So the SW registers at root scope, but the fetch handler returns early
(no event.respondWith call) for any URL outside /openstation/,
/wp-admin/, or the plugin's own assets directory. Behaviorally this is
"narrow scope" without inheriting the technical limitation.
If any other service worker (any scope) is already registered on the origin, the registration bails with a console warning rather than usurping it. The "Install <site> as an app" tile then surfaces a focused toast pointing at the opt-in filter (rather than the generic "not available" fallback), so users on affected sites see the actionable message instead of silently broken behaviour.
To opt this install in, return true from the
openstation_pwa_force_replace_sw filter:
add_filter( 'openstation_pwa_force_replace_sw', '__return_true' );The filter resolves at shell-config build time; effective on the next page load. Use this when another PWA plugin's SW is shadowing OpenStation and you want OpenStation to take over the install path.
| Pattern | Strategy | Why |
|---|---|---|
/wp-content/plugins/desktop-mode/assets/**.{css,png,jpg,svg,…} |
Stale-while-revalidate (runtime cache) | Returning users open the shell instantly; the SW updates the cache in the background. |
/wp-content/plugins/desktop-mode/assets/**.js |
Network-first with cache: 'reload' + cache fallback |
JS bundles change per deploy — a fresh deploy reaches online users on the next load, with no stale-revalidate window where a freshly-pushed fix is invisible. The cache still serves offline users. |
| Navigation requests under our scope | Network-first with offline fallback | wp-admin HTML carries nonces and per-request screen state; caching it would desynchronise the user. The fallback is a tiny inline placeholder so an offline user sees something coherent. |
| REST / AJAX / non-asset GETs | Pass-through (no SW handling) | Same reason as navigation — auth-bound dynamic content must hit the network. |
install-time precache |
A handful of CSS files, the three critical-path JS bundles (desktop.min.js, window-system.min.js, shell-overlays.min.js), and the plugin logo |
Just enough to render the offline shell skeleton. Anything else is picked up at runtime by the caching paths above. |
The cache is keyed by version (os-static-<v>,
os-runtime-<v>). The activate step deletes any cache whose
key doesn't carry the current version, so a deploy doesn't accumulate
stale buckets.
| Symbol | Role |
|---|---|
openstation_pwa_manifest_url() |
Absolute URL of the manifest endpoint. |
openstation_pwa_sw_url() |
Absolute URL of the service worker. |
openstation_pwa_get_user_state( $user_id = 0 ) |
Read the per-user PWA UI state. |
openstation_pwa_update_user_state( array $patch, $user_id = 0 ) |
Merge a partial update into the state. |
openstation_pwa_manifest (filter) |
Mutate manifest fields before encoding. |
REST routes:
-
GET /wp-json/desktop-mode/v1/pwa-state→{ installHintDismissed, notificationsEnabled } -
POST /wp-json/desktop-mode/v1/pwa-state→ merge partial state. Body:{ installHintDismissed?: bool, notificationsEnabled?: bool }.
Both routes require a logged-in user with OpenStation enabled for
their account (openstation_rest_require_enabled() — 401 when logged
out, 403 when OpenStation is off) and a valid X-WP-Nonce.
// All exposed on `window.wp.os`:
wp.os.notify( {
title: 'Build complete',
body: '12 files updated.',
icon: '/favicon.png',
tag: 'my-plugin/build',
onClick: ( n ) => { window.focus(); n.close(); },
} );
const choice = await wp.os.pwa.promptInstall();
// 'accepted' | 'dismissed' | 'unavailable'
await wp.os.pwa.requestNotificationPermission();
// 'granted' | 'denied' | 'default' | 'unsupported'
const state = wp.os.pwa.getState();
// { installHintDismissed: boolean, notificationsEnabled: boolean }
const off = wp.os.pwa.subscribe( ( s ) => {
console.log( 'PWA state changed:', s );
} );
off();Activity-bus channels for plugins that want to mute / amplify / audit:
-
os/notification-requested— filterable; setcancel: trueto suppress the underlying notification. -
os/notification-shown— fire-and-forget; carriesfallback: 'toast' | nullso analytics can distinguish the permission-denied path from the real-notification path.
For Chromium / Edge to fire beforeinstallprompt, the site needs:
- HTTPS (or
localhost). - A valid manifest with a
name(orshort_name), an icon ≥192×192, and astart_url. - A registered service worker that responds to a
fetchforstart_url(we do — the SW's network-first handler covers it). - The user to have engaged with the page for a few seconds (browser heuristic).
Safari (macOS / iOS) doesn't fire beforeinstallprompt. Users add the
app via the "Share → Add to Home Screen" gesture, which picks up our
apple-mobile-web-app-* meta tags emitted from admin_head.
-
Phase 4 — Web Push. VAPID keypair, REST routes for subscribe /
unsubscribe, server-side
openstation_push( $user_id, $payload )PHP helper, SWpushpayload renderer wired to the existingnotify()intent shape. The v1wp.os.notifyAPI is the same call site — only the transport changes. - Per-site icon override hint. A small OpenStation Preferences tab entry that lets administrators upload a custom PWA icon without writing a filter.
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
- Migration — WordPress package globals are no longer ambient
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config
- Place something where the user can reach it — wp.os.workArea