@fstage/push provides a single push API that routes to:
- native adapter (
@capacitor/push-notifications) when running in Capacitor - web adapter (Service Worker + PushManager + VAPID) in browser/PWA contexts
This lets app code call one service regardless of platform.
The native adapter implementation lives in @fstage/native and is exported as createNativePushAdapter.
Push adapters follow a Capacitor-shaped contract:
can()checkPermissions()requestPermissions()register(opts?)unregister(opts?)addListener(name, fn)removeAllListeners()
fstage also keeps compatibility helpers:
state()/requestPermission()subscribe(topic?)/unsubscribe(topic?)on(name, fn)topics()close(topic?)
Add @fstage/push to your libs phase and configure push at the top level:
export default {
loadAssets: {
preload: [ '@fstage/env', '@fstage/registry', '@fstage/stack' ],
libs: [
'@fstage/component',
'@fstage/store',
'@fstage/router',
'@fstage/native',
'@fstage/push',
],
app: [ 'js/components/app.mjs', 'css/style.css' ],
},
push: {
prefer: 'auto', // 'auto' | 'native' | 'web'
native: {
url: '/api/push/native',
},
web: {
url: '/api/push/web',
vapidKey: '<PUBLIC_VAPID_KEY>',
},
},
afterLoadPreload(e) { e.modules.get('stack.wirePreload', [ e ]); },
afterLoadLibs(e) { e.modules.get('stack.wireStack', [ e ]); },
afterLoadApp(e) { e.modules.get('stack.startStack', [ e ]); },
};When @fstage/stack is used, config.push is wired automatically into registry.get('push').
const registry = fstage.modules.get('registry.defaultRegistry', []);
const push = registry.get('push');
if (push && push.can()) {
await push.requestPermissions();
await push.register({ topic: 'tasks' });
}Facade methods:
mode(): 'native' | 'web'init(url, vapidOrOpts?)can(): booleancheckPermissions(): Promise<{ receive: 'granted'|'denied'|'prompt' }|false>requestPermissions(): Promise<{ receive: 'granted'|'denied'|'prompt' }|false>register(opts?): Promise<boolean>(opts.topicoptional)unregister(opts?): Promise<boolean>(opts.topicoptional)addListener(name, listener): Promise<{ remove(): void }>removeAllListeners(): Promise<void>topics(): string[]state(opts?): Promise<'granted'|'denied'|'prompt'|false>requestPermission(): Promise<'granted'|'denied'|'prompt'|false>subscribe(topic?): Promise<boolean>unsubscribe(topic?): Promise<boolean>close(topic?): Promise<boolean>on(name, listener): () => void(token,message,open,erroron native)destroy(): void
If you prefer explicit control, use the adapter directly by environment:
import { createNativePushAdapter } from '@fstage/native';
import { createWebPushAdapter } from '@fstage/push';
const isNative = !!(window.Capacitor && window.Capacitor.isNativePlatform());
const push = isNative
? createNativePushAdapter({ url: '/api/push/native' })
: createWebPushAdapter({ topicsKey: 'push.web.topics' });
if (!isNative) {
push.init('/api/push/web', '<PUBLIC_VAPID_KEY>');
}Most apps should use createPush() (auto routing), but both paths are supported.
Native mode uses Capacitor PushNotifications:
- requires native platform setup (iOS capability, Android FCM config)
config.push.native.urlis optional but recommended for token/topic sync- server payload shape:
{
"token": "<APNS_OR_FCM_TOKEN>",
"topics": ["tasks"],
"platform": "native"
}If no URL is configured, subscribe/unsubscribe still work locally but no backend sync is attempted.
- Install plugin and sync:
npm install @capacitor/push-notifications
npx cap sync- Configure Capacitor plugin options (
capacitor.config.jsonorcapacitor.config.ts):
{
"plugins": {
"PushNotifications": {
"presentationOptions": ["badge", "sound", "alert"]
}
}
}- iOS:
- Enable the Push Notifications capability in Xcode.
- Add the Capacitor registration callbacks in
AppDelegate.swift:
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
}- Android:
- Set up Firebase Cloud Messaging for the app.
- Add
google-services.jsontoandroid/app. - Configure channel/icon metadata if you need custom foreground/background behavior.
Web mode uses standard Web Push (no Firebase SDK required):
- HTTPS + service worker + PushManager support required
- call
init(url, vapidKey)(or setconfig.push.web.url+vapidKey) - server payload shape:
{
"subscription": { "...": "PushSubscription JSON" },
"topics": ["tasks"],
"platform": "web"
}Recommended backend behavior:
POSTcreate/update subscriptionPUTupdate topicsDELETEremove subscription
- Serve over HTTPS.
- Register a service worker.
- Generate VAPID keys and configure
config.push.web.vapidKey. - Expose a backend endpoint for subscription sync (
POST,PUT,DELETE) and push sending. - Ensure your push payload shape is compatible with your SW notification handler.
Push presentation config stays app-level in capacitor.config.json:
{
"plugins": {
"PushNotifications": {
"presentationOptions": ["badge", "sound", "alert"]
}
}
}This is not managed by fstage modules.