Notify guides
12 · Devices, logout and data-only push
Devices, logout and data-only push.
Keep the device list true: register on every start, unregister on logout, let Notify prune what Apple and Google report dead.
1
The lifecycle
- Register on every app start (and when the token changes). It is an upsert: the same token refreshes its
last_seen_at. - Shared devices: a token registered by another recipient of the workspace moves to the new one (at most 3 times a token per day), and a
device.movedwebhook fires. For 24 hours after a move, critical types also email the previous owner. - Pruning is automatic: APNs 410, FCM
UNREGISTEREDand web push 404 or 410 remove the device; so do 270 days without a registration. A recipient keeps at most 20 active devices: a 21st prunes the least recently seen.
2
Unregister on logout
Otherwise the next person to sign in on that phone gets the previous user’s pushes until the token moves. Unregister before you drop the session, because the call needs the user’s token.
sign-out.ts
import { createNotifyClient } from "@productcraft/notify-js";
const notify = createNotifyClient({ getToken: () => fetch("/api/notify-token").then((r) => r.text()) });
export async function signOut(deviceId: string | undefined) {
// Before you drop the session: the call needs the user's token.
if (deviceId) await notify.devices.unregister(deviceId);
notify.close(); // closes the stream, forgets the token
}Native apps call DELETE /v1/client/devices/{device_id} the same way.
3
From your backend
devices.ts
const devices = await notify.recipients.devices.list("user_123");
for (const d of devices.data) {
// state: active, invalid (e.g. a sandbox token sent to production), pruned
console.log(d.channel, d.platform, d.state, d.state_reason, d.last_seen_at);
}
// Sign this user out everywhere: their tokens stop working at once.
await notify.recipients.revokeTokens("user_123");4
Data-only (background) pushes
With data_only: true the push carries no alert: iOS receives it as a background notification (content-available, low priority, and the system may delay or drop it), Android as a data message your service handles. Use it to trigger a sync, never for something the user must see.
sync.ts
// No alert is shown: the app wakes in the background (iOS
// content-available, Android data message) and reads the data.
await notify.notificationTypes.replaceByKey("sync.requested", {
name: "Background sync",
category_key: "system",
routing: { always: ["push"], fallback: [] },
templates: [{ channel: "push", locale: "en", body: { body: "sync", data_only: true } }],
});