Appearance
Keryx iOS APNs delivery architecture
Approved: 25 August 2026
This document is the P1-04 production delivery architecture for Keryx killed-state notification delivery on iPhone and iPad. It defines topology, APNs device-token registration with the provisioned client ntfy host, upstream poll_request wakeups for com.sourcectl.keryxapp, environments, cache and rotation ownership, and honest Focus/force-quit limits.
It does not claim that token POST, upstream configuration, production APNs entitlement, or physical-device proof exist. Implementation, signing, deployer checklists, and device matrix remain later work.
Purpose and non-goals
Purpose
- Record why the current empty APNs token callback cannot launch.
- Approve an iOS killed-state architecture aligned with ntfy self-hosted
poll_requestsemantics and the Android delivery architecture. - Define token registration with the client ntfy host (not SafeCall).
- Assign signing, deployer, device-matrix, and claims handoffs.
Non-goals
- Changing Swift/Dart, Apple entitlements, or production ntfy servers.
- Implementing token forward, refresh, or delete in the vendored plugin.
- Running physical foreground/background/terminated tests (owned by #117).
- Completing production APNs signing (owned by #110).
- Introducing a SafeCall HTTP API that stores or brokers APNs tokens.
- Inventing numeric delivery SLOs.
- Distributing raw Apple APNs keys to every client deployer.
Approved decisions
- Owner and approver: Thomas Minitsios, 25 August 2026.
- Token provider: provisioned client ntfy HTTPS host from the QR (
ntfy_server_url), authenticated with the per-QR subscribe credential from the provisioning protocol. - Not the provider: SafeCall server HTTP APIs.
- Wake path: client ntfy caches alerts and emits upstream
poll_request; an APNs-capable upstream addresses iOS bundlecom.sourcectl.keryxapp; Keryx polls the client ntfy for cached bodies. - APNs credentials for
com.sourcectl.keryxapp: owned by sourcectl; not handed to every client as raw Apple keys. The team APNs.p8key is not App-ID-specific, but upstream ntfy must target topiccom.sourcectl.keryxapp. Launch default is client ntfyupstream-base-url(or equivalent) to an APNs-capable upstream that holds those credentials. Android remainscom.sourcectl.keryx(FCM) and is out of this architecture.
Current gap baseline
Architecture approval (25 August 2026) recorded the pre-implementation gap. Code status as of 13 September 2026:
| Surface | Current behavior | Architecture target |
|---|---|---|
| APNs registration | registerForRemoteNotifications() is called | Unchanged |
| Device addressing | APNs token is given to Firebase Messaging; Keryx subscribes to FCM topics ~poll + QR topics. No /v1/account/device PUT | Firebase-for-Keryx (stock ntfy Instant Notifications model for a custom app) |
| Token refresh / invalidation | Re-subscribe on APNs token; unsubscribe FCM topics on reset | Unchanged intent |
poll_request handling | Present; authenticated cache poll; event=message APNs skips a second local banner | Keep |
| Upstream / cache | How-to landed; operator rows OA-119-firebase / ntfy-fcm / ntfy-apns still pending | ntfy-fcm-apns.md |
| Signing entitlement | Store IPAs prove production aps-environment (#110). Source may stay development | Unchanged |
| SafeCall | Publishes alerts only | No token registration API |
Evidence sources:
keryx/vendor/ntfy/ios/ntfy/Sources/ntfy/NtfyPlugin.swiftkeryx/ios/Runner/AppDelegate.swift- Keryx application Notification Delivery and iOS Push
- Authenticated provisioning protocol
- Android delivery architecture
Topology and owners
| Role | Owner | Responsibility |
|---|---|---|
| Go/no-go and ntfy/push accountable | Thomas Minitsios (P0-05) | Approves topology; owns launch-blocker residual |
| Client ntfy host | Client deployer (#119) | HTTPS, ACL, cache/retention, base-url, upstream-base-url (or equivalent) |
APNs credentials for com.sourcectl.keryxapp | sourcectl | Production/sandbox APNs or Firebase project that can wake Keryx |
| Upstream APNs-capable ntfy | sourcectl-operated or deployer-configured path holding those credentials | Receives upstream poll_request; sends APNs wake |
| Token registration target | Provisioned QR ntfy_server_url | Keryx registers device token with subscribe_auth |
| SafeCall publisher | SafeCall server | Publishes alerts with publish token only; no APNs token API |
mermaid
flowchart LR
subgraph gap [CurrentGap]
APNsReg[APNs_device_token] --> Discard[Empty_callback]
PollHook[poll_request_handler] --> PollLater[Poll_without_token_reg]
end
subgraph approved [ApprovedArchitecture]
Token[APNs_token] -->|"POST_with_subscribe_auth"| ClientNtfy[Client_ntfy_HTTPS]
ClientNtfy -->|"upstream_poll_request"| Upstream[APNs_capable_upstream]
Upstream -->|"APNs_wake"| Keryx[Keryx]
Keryx -->|"HTTPS_poll_subscribe_auth"| ClientNtfy
endLaunch default: alert bodies remain on the client ntfy cache; upstream carries wake / poll_request metadata only, aligned with ntfy’s self-hosted iOS model. Per-client Apple key custody is out of scope for launch.
Token registration contract
Design intent for the implementing release (exact ntfy device API path is an implementation detail):
- Create / refresh: on initial APNs registration and on token change, forward the device token to the provisioned client ntfy over HTTPS using the per-QR
subscribe_auth. - Delete / revoke: on app reset, unsubscribe, or known token invalidation, delete or revoke the device registration on that ntfy.
- Transport: HTTPS only; system CA verification; same URL rules as provisioning protocol.
- Secrets: do not send tokens to SafeCall HTTP APIs; do not show raw tokens in ordinary or debug UI; do not put tokens in support tickets by default.
- Entitlement environment: store builds use production
aps-environment(#110); isolated test builds may use sandbox. Mismatched entitlement/token/upstream environment is a documented failure mode.
Rejected for this launch: SafeCall-as-token-broker (new authenticated SafeCall registration API that stores APNs tokens).
Delivery path
- SafeCall publishes an alert to the client ntfy with the publish token (P1-02).
- Client ntfy caches the message and emits an upstream
poll_request. - The APNs-capable upstream wakes Keryx for
com.sourcectl.keryxapp. - Keryx’s existing
poll_requesthandler runs a bounded authenticated HTTPS poll (?poll=1&since=…) against the client ntfy. - Swift stores/forwards each cached message and applies the native policy gate. Active/occurred alerts create an OS notification only when the category and device policy allows the first arrival in its rolling window. Resolved and snapshot state updates remain silent. Tombstone and history-cap rules are unchanged.
Foreground path
While Keryx is open, all QR topics use one comma-separated ntfy JSON stream rather than one URLSessionDataTask per category. Swift remains the only OS notification builder, using Dart-synchronized policy plus native-owned last-notified/dedupe state. This does not replace Tier 1 killed-state delivery.
Environments
| Environment | Purpose |
|---|---|
| Isolated reviewer/test ntfy | Production-equivalent HTTPS, ACL, cache, and upstream; no production alert data |
| Production client ntfy | Per SafeCall deployment; checklist in #119 |
| Apple sandbox APNs | Test/signing path; must not be mixed with production tokens upstream |
| Apple production APNs | Store builds after #110 proves entitlement and signing |
Configuring an isolated production-equivalent test path is an ops task for deployer/#119 and later P5 reviewer access. Closing this architecture item does not stand up that host.
Cache, monitoring, and rotation
- Cache / retention: deployer setting on the client ntfy. #119 records recommended bounds without inventing numeric SLOs here. Messages that expire before poll are missed.
- Monitoring: deployer-visible ntfy health plus SafeCall support intake. No analytics or crash SDK at launch (P0-05). #132 may approve minimum signals later.
- Rotation:
- APNs / Firebase credentials — sourcectl
- Upstream access token if used — deployer / sourcectl per #119
- Per-QR subscribe tokens — P1-02 lifecycle
- Publish and token-manager secrets — #119
Fallback tiers and honest limits
| Tier | Trigger | Behavior | Limitation |
|---|---|---|---|
| 1 | APNs poll_request | Authenticated poll + local notify | Needs token registration, upstream, cache, notification permission |
| 2 | App foreground / resume | Stream or poll-on-resume | No killed-state delivery without Tier 1 |
Honest platform limits for safety claims, #120, and #121:
- Force-quit blocks APNs wake until the administrator reopens Keryx.
- Focus / Do Not Disturb may suppress presentation.
- Notification permission denial blocks alerts.
- Cache expiry before poll loses the message.
- Network or upstream failure delays delivery until recovery.
- Without deployer upstream and cache, iOS remains Tier 2 only.
- Keryx is a supplemental administrator companion, not a guaranteed emergency or regulated medical channel.
Dependencies
- P1-02 provisioning protocol: HTTPS-only URLs;
subscribe_authon poll and on token registration requests. - P1-03 Android delivery: shared ntfy upstream
poll_requestsemantics; different OS push transport (FCM vs APNs). - #110: production APNs entitlement and distribution signing.
- #119: client ntfy HTTPS, ACL, cache,
upstream-base-url, and APNs-capable upstream.
Residual risks after architecture approval
| Risk | Residual after this design is implemented | Still open until |
|---|---|---|
| Empty APNs token callback | Removed; Firebase topic subscribe addresses the device | OA-119-firebase client files + new store binaries |
| No upstream / cache | Tier 2 only | #119 OA-119-ntfy-fcm / OA-119-ntfy-apns |
| Sandbox vs production APNs mismatch | Documented failure mode | #110 + test path |
| Force-quit / Focus | Honest limitation | safety-claims.md / #121 copy |
| Cache expiry | Possible missed alert | #119 retention + safety-claims.md |
| Unauthenticated poll or token POST | Same as P1-02 residual | P1-02 implementation |
Closing #97 approves this architecture. It does not:
- claim token POST, upstream, or production APNs entitlement exist in code;
- satisfy #110 signing, #117 physical-device matrix, or #119 checklist completion;
- claim guaranteed emergency delivery.
Implementation handoffs
| Work | Owner |
|---|---|
Firebase topic subscribe (not /v1/account/device) | Landed in vendored iOS plugin |
| Authenticated HTTPS poll on wake | Landed; depends on P1-02 |
| Production APNs entitlement / signing | #110 — ios-signing.md (done) |
Deployer Firebase + notify-host firebase-key-file | #119 — ntfy-fcm-apns.md |
| Physical foreground/background/terminated proof | #117 |
| Safety and delivery-limitation claims | #103 packet: safety-claims.md |
| Administrator troubleshooting | #120, #121 |
Review record
- Status: approved as the P1-04 iOS APNs delivery architecture.
- Date: 25 August 2026.
- Approver: Thomas Minitsios under the P0-05 RACI.
- Depends on: P1-01 threat model, P1-02 provisioning protocol, P1-03 Android delivery architecture.
- Evidence sources:
NtfyPlugin.swiftempty token callback andpoll_requesthooks,AppDelegate.swift,docs/mobile/keryx.md, ntfy self-hosted upstream/poll_requestmodel.