AbsoluteJS

Push notifications

One call in your page turns on notifications for iOS, Android and the web. Devices register as the signed-in user, and your server sends to people and topics while Dispatch handles APNs, FCM and Web Push.

#How it works

1
The page enables push
Your page turns notifications on, usually from a button. Permission is requested only if it has not been asked yet.
await pushNotifications.enable()
2
The device gets an address
The device gets its address from Apple (APNs) or Google (FCM); in a browser it is a Web Push subscription. Your page never sees it.
3
Your server registers the device
The app sends it to /auth/push as the signed-in user. Your server decides the user, organization and topics; the device cannot choose them.
4
You send to people, not devices
Dispatch stores the device and, when you send, delivers to every matching device through APNs, FCM or Web Push.

#In your pages

Push uses pushNotifications from @absolutejs/devices. Importing it is what adds the native push plugin and its permissions to your app.

TS
import { pushNotifications } from '@absolutejs/devices';

// From a "Turn on notifications" button. Asks for permission if it
// has not been asked yet, then registers this device with your server.
await pushNotifications.enable();

// Stop sending to this device.
await pushNotifications.disable();
Follows sign-inWhen someone signs in and has already allowed notifications, their device is registered again without a prompt.
Stops at sign-outSigning out removes the device from that account first, so the next person to use the phone gets nothing meant for the last.
Survives token changesWhen APNs or FCM rotates the address, the same installation is updated rather than added twice.

Listen for notifications while the app is open, and for taps. The deepLink you sent arrives in notification.data.absoluteDeepLink; route to it with your app’s own navigation.

TS
import { pushNotifications } from '@absolutejs/devices';

// A notification arrived while the app was open.
const stopReceived = await pushNotifications.onReceived((notification) => {
  showToast(notification.title, notification.body);
});

// The user tapped a notification or one of its actions.
const stopActions = await pushNotifications.onAction(({ actionId, notification }) => {
  const link = notification.data.absoluteDeepLink;
  if (actionId === 'tap' && typeof link === 'string') openInApp(link);
});

// When the component unmounts
await stopReceived();
await stopActions();

#On your server

Create one push lifecycle with an adapter for each kind of device, and storage for registrations. Apply PUSH_SUBSCRIPTION_POSTGRES_SCHEMA from @absolutejs/dispatch-push-postgres and IDEMPOTENT_OPERATION_POSTGRES_SCHEMA from @absolutejs/reliability to your database once.

TS
import { createPushLifecycle } from '@absolutejs/dispatch';
import { createApnsAdapter } from '@absolutejs/dispatch-apns';
import { createFcmAdapter } from '@absolutejs/dispatch-fcm';
import {
  createPostgresPushFanoutClaimStore,
  createPostgresPushSubscriptionStore
} from '@absolutejs/dispatch-push-postgres';
import { createWebPush } from '@absolutejs/pwa';
import { createWebPushDispatchAdapter } from '@absolutejs/pwa/dispatch';
import {
  createPostgresIdempotentOperationStore,
  createPostgresTransactionRunner
} from '@absolutejs/reliability';

const apns = createApnsAdapter({
  bundleId: 'com.example.shop',
  keyId: process.env.APNS_KEY_ID!,
  privateKey: process.env.APNS_PRIVATE_KEY!,
  teamId: process.env.APNS_TEAM_ID!
});
const fcm = createFcmAdapter({ projectId: process.env.FCM_PROJECT_ID! });
const webPush = createWebPush({
  publicKey: process.env.VAPID_PUBLIC_KEY,
  privateKey: process.env.VAPID_PRIVATE_KEY,
  subject: 'mailto:alerts@example.com'
});

const runner = createPostgresTransactionRunner(pool);

export const pushLifecycle = createPushLifecycle({
  adapterFor: (subscription) => {
    if (subscription.platform === 'apns') return apns;
    if (subscription.platform === 'fcm') return fcm;

    return createWebPushDispatchAdapter(webPush, subscription);
  },
  claimStore: createPostgresPushFanoutClaimStore(
    createPostgresIdempotentOperationStore(runner)
  ),
  store: createPostgresPushSubscriptionStore(runner)
});

Then give it to Auth. That mounts /auth/push, which accepts the app’s signed-in requests and the browser’s session cookie. Push needs Auth’s oidc provider, which your app already uses to sign in.

TS
import { auth } from '@absolutejs/auth';
import { pushLifecycle } from './push';

const authApplication = await auth({
  getUser,
  providersConfiguration,
  oidc: oidcConfiguration,
  push: {
    registrar: pushLifecycle,
    tenant: (principal) => principal.user.organizationId,
    topics: (principal) => ['orders', `store:${principal.user.storeId}`]
  }
});
The build checks this
A mobile build stops with a message if a page imports pushNotifications but @absolutejs/auth is not installed or auth({ push }) is not configured.

#Sending

TS
// Every device of one user
await pushLifecycle.send(
  { tenant: 'acme', userId: order.customerId },
  {
    title: 'Your order shipped',
    body: 'Arriving Thursday.',
    deepLink: '/orders/' + order.id,
    idempotencyKey: `order:${order.id}:shipped`
  }
);

// Everyone subscribed to a topic
await pushLifecycle.send(
  { tenant: 'acme', topic: 'orders' },
  { title: 'New order', body: 'Order 1042 is waiting.' }
);
Send toReaches
{ tenant, userId }Every device of one user.
{ tenant, topic }Every device subscribed to a topic.
{ tenant, deviceId }One device.
{ tenant, subscriptionIds }Specific registrations.
Message fieldMeaning
bodyThe text. Required.
titleThe heading.
deepLinkWhere a tap should go. Arrives as data.absoluteDeepLink in the app, and opens that URL from a browser notification.
dataExtra values your app reads in onReceived or onAction.
badgeThe number on the app icon.
soundA notification sound.
actionsButtons shown on the notification.
idempotencyKeyMakes a retry of the same send deliver once.

#Platform setup

PlatformYou needWhat AbsoluteJS does
iOSAn APNs key (.p8) from your Apple Developer account, with its key ID and your team ID.The push entitlement and the code that receives the device address are added to the iOS project for you. For builds run from Xcode, create the APNs adapter with environment set to sandbox.
AndroidA Firebase project with an Android app whose package name is your appId.Put its google-services.json in your project; absolute mobile sync checks it matches your appId and copies it in. Your server sends with a Google service account.
WebA VAPID key pair.Add a pwa block to absolute.config.ts. Pages that import pushNotifications get Web Push in the browser and the installed web app.
TS
mobile: {
  appId: 'com.example.shop',
  appName: 'Shop',
  server: { productionOrigin: 'https://shop.example.com' },
  pushNotifications: {
    android: { googleServicesFile: 'google-services.json' } // the default path
  }
}

The same pushNotifications code sends Web Push to the browser and the installed web app once your config has a pwa block and the build has a public VAPID key.

TS
// absolute.config.ts: turn on the installable web app
pwa: {
  manifest: {
    name: 'Shop',
    shortName: 'Shop',
    icons: [{ src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' }]
  }
}

// .env: the public key is built into the page; the private key stays on the server
// VAPID_PUBLIC_KEY=...
// VAPID_PRIVATE_KEY=...

#Delivery guarantees

Dead devices removedAn address APNs, FCM or the browser reports as gone is retired, so you stop paying for sends that cannot arrive.
Retries with limitsTemporary failures are retried, three attempts by default, with ten sends in flight at once.
No double alertsIf a provider’s answer is lost, the send is recorded as indeterminate and not retried, so nobody gets the same alert twice.
Tenant isolationEach registration belongs to one organization. A send can never reach another tenant’s devices.

#Local notifications

For reminders the app can schedule itself, use localNotifications instead. They need no server and fire with no connection.

FeatureLocal notificationsPush notifications
Who sends itYour app, on the deviceYour server
Works with no connection
Needs Auth and a server
Typical useReminders, timers, pickupsMessages, orders, alerts
TS
import { localNotifications } from '@absolutejs/devices';

const permission = await localNotifications.requestPermission();
if (permission.state === 'granted') {
  await localNotifications.schedule({
    id: 7,
    title: 'Pickup reminder',
    body: 'Your order is ready at 5 pm.',
    scheduledAtMs: pickupTime - 30 * 60 * 1000
  });
}

#Packages

Every device feature, including the permission states push uses, is on Device APIs.