Skip to content

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_request semantics 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 bundle com.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 .p8 key is not App-ID-specific, but upstream ntfy must target topic com.sourcectl.keryxapp. Launch default is client ntfy upstream-base-url (or equivalent) to an APNs-capable upstream that holds those credentials. Android remains com.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:

SurfaceCurrent behaviorArchitecture target
APNs registrationregisterForRemoteNotifications() is calledUnchanged
Device addressingAPNs token is given to Firebase Messaging; Keryx subscribes to FCM topics ~poll + QR topics. No /v1/account/device PUTFirebase-for-Keryx (stock ntfy Instant Notifications model for a custom app)
Token refresh / invalidationRe-subscribe on APNs token; unsubscribe FCM topics on resetUnchanged intent
poll_request handlingPresent; authenticated cache poll; event=message APNs skips a second local bannerKeep
Upstream / cacheHow-to landed; operator rows OA-119-firebase / ntfy-fcm / ntfy-apns still pendingntfy-fcm-apns.md
Signing entitlementStore IPAs prove production aps-environment (#110). Source may stay developmentUnchanged
SafeCallPublishes alerts onlyNo token registration API

Evidence sources:

Topology and owners

RoleOwnerResponsibility
Go/no-go and ntfy/push accountableThomas Minitsios (P0-05)Approves topology; owns launch-blocker residual
Client ntfy hostClient deployer (#119)HTTPS, ACL, cache/retention, base-url, upstream-base-url (or equivalent)
APNs credentials for com.sourcectl.keryxappsourcectlProduction/sandbox APNs or Firebase project that can wake Keryx
Upstream APNs-capable ntfysourcectl-operated or deployer-configured path holding those credentialsReceives upstream poll_request; sends APNs wake
Token registration targetProvisioned QR ntfy_server_urlKeryx registers device token with subscribe_auth
SafeCall publisherSafeCall serverPublishes 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
  end

Launch 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

  1. SafeCall publishes an alert to the client ntfy with the publish token (P1-02).
  2. Client ntfy caches the message and emits an upstream poll_request.
  3. The APNs-capable upstream wakes Keryx for com.sourcectl.keryxapp.
  4. Keryx’s existing poll_request handler runs a bounded authenticated HTTPS poll (?poll=1&since=…) against the client ntfy.
  5. 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

EnvironmentPurpose
Isolated reviewer/test ntfyProduction-equivalent HTTPS, ACL, cache, and upstream; no production alert data
Production client ntfyPer SafeCall deployment; checklist in #119
Apple sandbox APNsTest/signing path; must not be mixed with production tokens upstream
Apple production APNsStore 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

TierTriggerBehaviorLimitation
1APNs poll_requestAuthenticated poll + local notifyNeeds token registration, upstream, cache, notification permission
2App foreground / resumeStream or poll-on-resumeNo 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_auth on poll and on token registration requests.
  • P1-03 Android delivery: shared ntfy upstream poll_request semantics; 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

RiskResidual after this design is implementedStill open until
Empty APNs token callbackRemoved; Firebase topic subscribe addresses the deviceOA-119-firebase client files + new store binaries
No upstream / cacheTier 2 only#119 OA-119-ntfy-fcm / OA-119-ntfy-apns
Sandbox vs production APNs mismatchDocumented failure mode#110 + test path
Force-quit / FocusHonest limitationsafety-claims.md / #121 copy
Cache expiryPossible missed alert#119 retention + safety-claims.md
Unauthenticated poll or token POSTSame as P1-02 residualP1-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

WorkOwner
Firebase topic subscribe (not /v1/account/device)Landed in vendored iOS plugin
Authenticated HTTPS poll on wakeLanded; depends on P1-02
Production APNs entitlement / signing#110ios-signing.md (done)
Deployer Firebase + notify-host firebase-key-file#119ntfy-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