Skip to content

From apps: the JavaScript client

client.push: register a device, keep it registered, subscribe a browser, and what mobile apps do with the raw endpoints.

The JavaScript client has client.push, a small layer over the device endpoints. It needs a user session: an API key can’t register devices.

const device = await backd.push.register({ platform: 'android', token: fcmToken, name: "Ana's Pixel" })
const mine = await backd.push.devices()           // [{ id, platform, name, created_at, last_seen }] (never a token)
await backd.push.unregister()                     // the device this client registered; or unregister(id)
CallWhat it does
register({ platform, token, name? })POST /_auth/devices. Registers or refreshes; keeps the returned id (client.push.deviceId) for unregister()
devices()GET /_auth/devices
unregister(id?)DELETE /_auth/devices/{id}; without an id, the device this client registered (false when there is none)
registerOnLaunch(getToken, { onError })Registers now if signed in and again after every sign-in. getToken returns what to register, or null while the app has no token. Returns a function that stops it
subscribeWeb({ registration, name?, requestPermission? })Subscribes a browser and registers the subscription (below)

Keeping a device registered

A device is removed when its session ends, when it is unseen for push.devices.stale_after, and when its realm’s web key is rotated. So an app registers at every launch and after every sign-in, which is what registerOnLaunch does:

const stop = backd.push.registerOnLaunch(async () => {
  const token = await getFcmToken()               // your platform's call
  return token ? { platform: 'android', token } : null
}, { onError: (e) => console.warn('push registration failed', e) })

A browser

subscribeWeb runs the whole sequence: it fetches the realm’s public key, asks for the notification permission, calls pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }) and registers the subscription as a web device.

const registration = await navigator.serviceWorker.register('/sw.js')
const { permission, device } = await backd.push.subscribeWeb({ registration })
// permission: 'granted' | 'denied' | 'default' | 'unsupported'; device is null unless granted

A denied permission isn’t an error: nothing is subscribed and the answer says so. Ask from a user gesture (a click), as browsers require.

The service worker

The push arrives in a service worker, which shows the notification and opens its link when it is tapped. This one is the example the repository carries (clients/js/examples/push/sw.js): backd’s sender delivers { title, body, url, data, tag }.

self.addEventListener('push', (event) => {
  const { title = 'New notification', body = '', url, data, tag } = event.data ? event.data.json() : {}
  event.waitUntil(self.registration.showNotification(title, { body, tag, data: { url, ...data } }))
})

self.addEventListener('notificationclick', (event) => {
  event.notification.close()
  const url = event.notification.data?.url
  if (url) event.waitUntil(self.clients.openWindow(url))
})

tag is the push’s group: a newer push with the same tag replaces the earlier one.

Mobile apps

There is no mobile client yet; an app calls the endpoints itself, at each launch and after signing in:

POST /v1/acme/_auth/devices
Authorization: Bearer <session token>
Content-Type: application/json

{"platform": "ios", "token": "<APNs device token>", "name": "Ana's iPhone"}

android takes the FCM registration token; ios the APNs device token (or the FCM token when the realm’s ios provider is fcm). See Devices for the endpoints and their rules.

The example

The expenses with functions example sends a push when the receiver of a settlement confirms it: confirm_settlement declares push: { kinds: [settlement-confirmed] } and calls ctx.push.send({ user: entry._meta.owner, kind: "settlement-confirmed", … }). Its realm uses the capture provider, and the page registers a demo device with registerOnLaunch, so the push shows up in the Mailbox’s Push tab (Local development).

Edit this page · Last updated 15 Oct 2026