Engagement
Reach customers across channels

Push notifications

Pulse delivers push to three surfaces from a single campaign: Web Push (browsers and PWAs via VAPID), Apple Push (APNs for iOS/macOS), and Android (FCM/HMS). One audience, one schedule, one analytics view.

How it works

  1. The SDK registers a device token after the user grants permission and stores it on device_tokens, scoped to the customer and the app slug.
  2. You author a push from Campaigns → New → Push or from a Push node in a journey.
  3. The push-dispatch edge function fans out to every active token, signs the request (VAPID JWT for web, APNs JWT for iOS, FCM HTTP v1 for Android), and records delivery + click events.

Pushes respect every safeguard the rest of Pulse uses: workspace-level frequency caps, per-channel quiet hours, suppression lists, and consent state.

Set up an app for push

Each App in Settings → Apps carries the credentials for one surface. Create one app per platform.

Web Push (browsers)

  1. Open your web app in Settings → Apps.
  2. Click Generate VAPID keys. Pulse writes the public key, private key, and contact subject onto apps.vapid_public_key, apps.vapid_private_key, apps.vapid_subject. The private key never leaves the server.
  3. Copy the vapid_public_key into your front-end and pass it to pulse.registerDevice():
// browser
await pulse.registerDevice({
  platform: 'web',
  token: subscriptionJSON,    // PushSubscription.toJSON() from the browser
  app_slug: 'consumer-web',
})

The browser obtains subscriptionJSON via the Service Worker:

const reg = await navigator.serviceWorker.register('/sw.js')
const sub = await reg.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
})
await pulse.registerDevice({ platform: 'web', token: sub.toJSON(), app_slug: 'consumer-web' })

iOS (APNs)

  1. Create an iOS app in Settings → Apps. Set the bundle_id (e.g. com.yourco.app) and choose the apns_environment (development for TestFlight, production for App Store).
  2. Upload your APNs auth key (.p8) under Integrations → Credentials. Pulse stores it in the private_secrets schema, accessible only to the dispatch function.
  3. In your iOS app, request user permission and forward the device token from application(_:didRegisterForRemoteNotificationsWithDeviceToken:):
Pulse.shared.registerDevice(platform: .ios, token: deviceTokenHex, appSlug: "consumer-ios")

Android (FCM)

  1. Create an Android app in Settings → Apps. Set the package name as bundle_id.
  2. Upload the FCM service account JSON under Integrations → Credentials.
  3. In your Android app, get the FCM token and register it:
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
  Pulse.registerDevice(platform = "android", token = token, appSlug = "consumer-android")
}

Authoring a push

From Campaigns → New campaign pick channel push. The editor exposes:

FieldNotes
TitleBold first line. Web/iOS show ~50 chars; Android shows ~65.
BodyUp to ~150 chars on most surfaces. Liquid variables are supported.
Image URLOptional rich-media image. Web Push uses it as the notification icon; iOS uses it for UNNotificationAttachment; Android uses it as BigPictureStyle.
Click URLWhere the device should navigate when tapped.
AppWhich app slug to dispatch through. Filters tokens to that surface.

Scheduling and frequency

Every push (campaign or journey node) supports the same scheduling fields the rest of the engagement surfaces use:

  • Starts at — earliest the dispatch may run. If unset, it dispatches immediately on activation.
  • Ends at — latest the dispatch may run. After this, the campaign auto-archives even if not all tokens were delivered (rare).
  • Display frequency:
    • once — fire one notification per device.
    • daily / weekly — repeat at this cadence between starts at and ends at.
    • persistent — for sticky/foreground notifications on Android; the OS keeps the notification visible until the user dismisses it.

Quiet hours and frequency caps still apply on top of these fields, so a daily push won't fire at 3am if your workspace has quiet hours configured.

Permission UX

Browsers and iOS show a one-time permission prompt; once denied, the OS won't show it again until the user manually re-enables in settings. Don't fire requestPermission() on first page load — wait for an action that justifies the prompt (sign-up, opt-in checkbox, "save for later").

Test mode

Pushes published in test mode only fan out to devices registered with a test SDK key (ppk_test_… / pk_test_…). Production devices are unaffected. Use this to verify payloads, deep links, and badge counts without spamming customers.

Troubleshooting

  • Web Push: 410 Gone — the subscription is dead. Pulse marks the token revoked_at = now() and stops sending.
  • iOS: BadDeviceToken — usually a TestFlight build hitting the production APNs environment. Set apns_environment = 'development' on the app.
  • Android: empty deliveries — confirm the FCM service account JSON is uploaded and the SHA-256 fingerprint of the signing key matches the one in the Firebase console.