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_tokenstable binding each APNs/FCM token to one user. PUT /api/app/v1/device-tokens— idempotent upsert (supportsIdempotency-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 (
.p8key, no certificates),sandboxandproductionenvironments.
Important files
| File | Role |
|---|---|
src/lib/db/schema/app.ts | device_tokens table |
src/server/app/devices/deviceTokenService.ts | Upsert/delete with ownership rules |
src/server/app/push/apns.ts | APNs HTTP/2 client (JWT auth, environment hosts) |
src/server/app/push/pushTestService.ts | Test-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:
sandboxfor Xcode debug builds,productionfor TestFlight/App Store tokens — a token only works against the environment it was issued for. - Deeplink: only
soar:orhttps: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.
