Appearance
Keryx Android background delivery architecture
Approved: 25 August 2026
This document is the P1-03 production delivery architecture for Keryx on Android under Android 15+/API 35–36 and current Google Play foreground-service policy. It replaces the perpetual dataSync foreground service with FCM poll_request upstream push plus authenticated HTTP poll, mirroring the iOS poll_request model already documented for Keryx.
It does not claim that FCM wiring, token registration, FGS removal, or Play Console declarations exist in code. Implementation, deployer checklists, Play forms, and physical-device proof remain later work.
Purpose and non-goals
Purpose
- Record why the current perpetual
dataSync/boot design cannot launch. - Approve an Android delivery architecture that is compatible with Android 15+ FGS timeouts and Play FGS eligibility rules.
- Define primary, foreground, and fallback delivery tiers with honest limits.
- Assign Play declaration, safety-claims, deployer, and device-matrix handoffs.
Non-goals
- Changing Android manifests, the vendored ntfy plugin, or Firebase packages.
- Submitting Play Console forms or uploading a demonstration video.
- Running physical-device, OEM, Doze, or soak tests (owned by #117).
- Accepting perpetual
dataSyncas launch-valid. - Inventing numeric delivery SLOs.
Approved decisions
- Owner and approver: Thomas Minitsios, 25 August 2026.
- Primary background path: ntfy upstream FCM
poll_requestwakes Keryx; Keryx performs a bounded authenticated HTTPS poll and shows local notifications for newly cached messages. - Foreground path: in-app stream or short poll while the UI is open; no perpetual background FGS and no indefinite wake lock for launch.
- Deprecated for launch: perpetual
dataSyncFGS withSTART_STICKY, indefinite wake lock, and automatic boot/locked-boot/package-replaced FGS restart.
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 |
|---|---|---|
| Vendored service | No perpetual dataSync FGS, boot receiver, or indefinite wake lock. Foreground multiplex SSE is a thread, not an Android ForegroundService | No perpetual background FGS for launch |
| Boot receiver | Removed | No automatic FGS restart from boot or package replace |
| Android push | FCM service polls on event=message and event=poll_request; subscribes to ~poll + QR topics | FCM wake → authenticated poll |
| iOS (reference) | Firebase topic subscribe + APNs; see iOS APNs delivery architecture | Shared ntfy / Firebase semantics |
| Poll recovery | Authenticated ?poll=1&since= (or since=all when empty) | provisioning protocol |
| Play forms / FCM host | Draft packet exists; real google-services.json and notify-host firebase-key-file still operator work | #119 / ntfy-fcm-apns.md |
Evidence sources for current code:
keryx/vendor/ntfy/android/src/main/AndroidManifest.xmlkeryx/vendor/ntfy/android/src/main/kotlin/io/karsmiths/ntfy/NtfyFirebaseMessagingService.ktkeryx/vendor/ntfy/android/src/main/kotlin/io/karsmiths/ntfy/NtfySubscriptionManager.ktkeryx/vendor/ntfy/ios/ntfy/Sources/ntfy/NtfyPlugin.swift- Keryx application Notification Delivery section
- Firebase / FCM / APNs how-to
Policy assessment
Android 14+ foreground service types
Play requires an accurate foreground-service type, eligibility, declaration, and evidence when an FGS is used (Play FGS requirements). A perpetual network listener that keeps SafeCall alerts streaming is not assumed to qualify as dataSync. Declaring dataSync for that use risks rejection even before timeout behavior is considered.
Android 15+ timeouts and start restrictions
Android 15+ applies an aggregate six-hour dataSync timeout in a 24-hour period and restricts background and boot starts for many FGS types (FGS timeouts). The current design assumes a sticky, boot-restarted, indefinite service. That assumption fails on the launch target (API 35/36, Play target API 36 from 31 August 2026).
API 35/36 expectations
Build and test against API 35/36 timeout, onTimeout, background-start, and BOOT_COMPLETED behavior are required before launch. Implementation and physical proof belong to later work and #117. Closing this architecture item does not satisfy those tests.
Verdict
The perpetual dataSync/boot design is not launch-valid. Keryx must adopt an approvable non-perpetual-FGS architecture before signing and listing work is treated as sufficient.
Approved architecture
mermaid
flowchart LR
subgraph deprecated [DeprecatedForLaunch]
Boot[BootReceiver] --> FGS[Perpetual_dataSync_FGS]
FGS --> Stream[Long_lived_HTTP_stream]
end
subgraph approved [ApprovedArchitecture]
Ntfy[ntfy_server] -->|"FCM_poll_request"| FCM[Firebase]
FCM --> App[Keryx]
App -->|"HTTPS_poll_with_subscribe_auth"| Ntfy
Foreground[App_foreground] -->|"one_multiplexed_stream"| Ntfy
endPrimary path (background, screen off, killed)
- The client ntfy host is configured for upstream Firebase push (for example
upstream-base-urlor equivalent deployer settings owned by #119). - When SafeCall publishes an alert, ntfy delivers an FCM
poll_request(or equivalent wake) to Keryx. With authenticated topics, the FCM payload must not carry the full alert body; the client polls for content. - Keryx wakes, performs a bounded authenticated HTTPS poll (
?poll=1&since=…) using the per-QR subscribe token from the provisioning protocol. - The native policy gate stores/forwards every newly cached message, then decides whether to create an OS notification from the category policy, per-device override, message phase/intent, and rolling-window timestamp. Existing tombstone and history-cap rules continue to apply.
Foreground path (app open)
- While the administrator has Keryx open, the selected topics are joined into ntfy's comma-separated JSON endpoint. One foreground SSE connection provides low-latency updates regardless of whether the QR contains one or eight categories.
- That path must not require a perpetual background FGS or an indefinite wake lock.
- Kotlin remains the only OS-notification builder. Dart synchronizes immutable category/device policy; Kotlin owns atomic message dedupe and last-notified timestamps so FCM and foreground delivery use the same gate.
Explicitly deprecated for launch
- Perpetual
dataSyncFGS withSTART_STICKYand indefinitePARTIAL_WAKE_LOCK. - Automatic FGS restart from
BOOT_COMPLETED,LOCKED_BOOT_COMPLETED, orMY_PACKAGE_REPLACED. - Claiming “instant delivery” via an always-on foreground notification for self-hosted ntfy (the pattern used by the generic ntfy Play app for self-hosted servers). That pattern is not approvable for Keryx’s launch target under current timeout and Play eligibility rules.
Rejected alternatives
| Alternative | Why rejected for launch |
|---|---|
| Redesign perpetual FGS (different type or shorter sticky loops) | Still hits Android 15+ aggregate timeout and Play type-eligibility risk for a 24/7 listener |
| WorkManager-only periodic polling as primary path | Too slow and opportunistic for administrator alerts; may be a non-primary supplement only if implementation needs it |
| UnifiedPush distributor mode | Out of scope for a first-party Google Play administrator companion |
Fallback tiers
| Tier | Trigger | Behavior | Limitation |
|---|---|---|---|
| 1 | FCM poll_request | Authenticated poll + local notify | Requires Google Play services, deployer FCM/upstream config, and accepts FCM latency |
| 2 | App foreground / resume | Existing poll-on-resume / startup poll | No delivery while killed without Tier 1 |
| 3 | Documented Tier 2 OEM battery exception (P0-04) | Unrestricted battery or OEM-specific settings with client-IT approval | Device-specific; Tier 1 must not silently depend on undocumented exceptions |
Honest platform limits that must appear in safety and support copy (safety claims, #120, #121):
- Force-stop / force-quit prevents background delivery until the administrator reopens Keryx.
- Do Not Disturb, Focus modes, notification permission denial, and OEM battery managers can suppress or delay alerts.
- Without deployer FCM/upstream configuration and message caching, Android falls back to Tier 2 only.
- Keryx is a supplemental administrator companion, not a guaranteed emergency or regulated medical channel.
Dependencies
- P1-02 provisioning protocol: subscribe auth on poll and any residual stream paths; HTTPS-only ntfy URLs.
- #119: client ntfy prerequisites including TLS, ACL, token manager, FCM / upstream-base-url, and message caching.
- P1-04 / #97: iOS APNs productization uses the same ntfy upstream
poll_requestsemantics with a different OS push transport. Architecture approved in ios-delivery-architecture.md.
Play Console and residual FGS posture
After implementation removes or minimizes the perpetual FGS:
- Prefer a release AAB that does not declare
FOREGROUND_SERVICE_DATA_SYNCor adataSyncservice unless a short, clearly eligible residual use remains. - Approved Data Safety, permission, App content, and FGS declaration answers are recorded in Play Data Safety (P1-08 / #101).
- A Play demonstration video is required only if a residual declared FGS type remains in the submitted AAB.
- Boot receivers that no longer start an FGS must not be left as misleading “always-on” claims in support or listing copy.
Residual risks after architecture approval
| Risk | Residual after this design is implemented | Still open until |
|---|---|---|
Android 15+ dataSync timeout / Play rejection of perpetual FGS | Removed by deprecating perpetual FGS | Implementation removes FGS/boot restart |
| Missed alerts without FCM/upstream | Tier 2 only | #119 deployer config + implementation |
| FCM latency or Play-services absence | Delayed or no background wake | Support claims (safety-claims.md); Tier 1 OEM matrix (#117) |
| Force-stop | No restart until reopen | Documented limitation (safety-claims.md / #121) |
| OEM battery killers | May still block wakes | P0-04 Tier 2 exceptions; #117 evidence |
| Unauthenticated poll | Same as P1-02 residual | P1-02 implementation |
Closing #96 approves this architecture. It does not:
- claim FCM, token registration, or FGS removal exist in code;
- accept perpetual
dataSyncas launch-valid; - satisfy the P4-03 physical-device matrix or Play Console submission.
Implementation handoffs
| Work | Owner |
|---|---|
Remove/minimize FGS, change boot/package-replace behavior, add Android FCM/poll_request handling | Later implementation in vendored plugin + Keryx |
| Authenticated HTTPS poll on wake (subscribe token) | Later implementation; depends on P1-02 |
| ntfy FCM / upstream-base-url deployer checklist | #119 |
| Play permissions / FGS declarations and demo | #101 packet: play-data-safety.md; console submission #125 |
| Safety and delivery-limitation claims | #103 packet: safety-claims.md |
| Physical-device matrix (closed/reboot/Doze/force-stop/pause/soak) | #117 |
| Administrator troubleshooting / battery guidance | #120, #121 |
Review record
- Status: approved as the P1-03 Android delivery architecture.
- Date: 25 August 2026.
- Approver: Thomas Minitsios under the P0-05 RACI.
- Depends on: P1-01 threat model, P1-02 provisioning protocol.
- Evidence sources: vendored Android service/boot receiver/manifests, iOS
poll_requesthooks,docs/mobile/keryx.md, Android 15 FGS timeout guidance, Play FGS declaration policy.