Push Notifications

APNs device-token registration and the authenticated test-push endpoint.

Push support is integrated for the full loop a native app needs during setup: register the device's APNs token against the signed-in user, send a real test notification to your own devices over APNs HTTP/2 (sandbox or production), and have invalid tokens cleaned up automatically. It is a self-serve verification tool — not a general campaign/broadcast system.

What it provides

  • A device_tokens table binding each APNs/FCM token to one user.
  • PUT /api/app/v1/device-tokens — idempotent upsert (supports Idempotency-Key).
  • DELETE /api/app/v1/device-tokens/:token — remove a token (sign-out).
  • POST /api/app/v1/notifications/test-push — send a test alert to the current user's own iOS devices.
  • Token-based APNs auth (.p8 key, no certificates), sandbox and production environments.

Important files

FileRole
src/lib/db/schema/app.tsdevice_tokens table
src/server/app/devices/deviceTokenService.tsUpsert/delete with ownership rules
src/server/app/push/apns.tsAPNs HTTP/2 client (JWT auth, environment hosts)
src/server/app/push/pushTestService.tsTest-push orchestration + invalid-token cleanup

Device tokens

A token identifies a device install, so it is globally unique in the table; re-registering an existing token under a different signed-in user reassigns ownership (the device changed accounts). Ownership always comes from the session — a client can only ever register tokens to itself, and deleting another user's token behaves as not-found.

PUT /api/app/v1/device-tokens          { "token": "<hex>", "platform": "ios" }
DELETE /api/app/v1/device-tokens/<token>

Register on launch/sign-in when iOS delivers the token; delete on sign-out.

APNs configuration

The server signs APNs requests with a token-based provider key (App Store Connect → Users and Access → Integrations → Keys, an APNs Auth Key .p8):

APNS_TEAM_ID=# Apple Developer team ID
APNS_KEY_ID=# The .p8 key's ID
APNS_BUNDLE_ID=com.example.app   # Sent as apns-topic
APNS_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----"

APNS_PRIVATE_KEY accepts \n escapes, so the .p8 contents fit in one env line. These are server-only secrets — they are read lazily by the test-push endpoint and must never reach a client.

The test-push endpoint

POST /api/app/v1/notifications/test-push
{ "title?": "…", "body?": "…", "deeplink?": "soar:///notifications", "environment?": "sandbox" }
  • Scope: sends only to the current session user's iOS tokens, capped at 10 devices. There is no way to target another user.
  • Environment: sandbox for Xcode debug builds, production for TestFlight/App Store tokens — a token only works against the environment it was issued for.
  • Deeplink: only soar: or https: URLs are accepted; anything else falls back to the default.
  • Rate limit: 5 requests per user per minute.
  • Response: { environment, sent, failed, deliveries } with a per-device status and the token's last 8 characters for identification.

Invalid-token cleanup

When APNs answers BadDeviceToken or Unregistered, the corresponding row is deleted automatically — stale tokens from reinstalls or environment mismatches disappear on the next test push instead of accumulating.

DEVICE_TOKEN_NOT_FOUND (404) from test-push means the user has no registered iOS tokens — register the device first, then send.

On this page