Skip to content

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 dataSync as launch-valid.
  • Inventing numeric delivery SLOs.

Approved decisions

  • Owner and approver: Thomas Minitsios, 25 August 2026.
  • Primary background path: ntfy upstream FCM poll_request wakes 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 dataSync FGS with START_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:

SurfaceCurrent behaviorArchitecture target
Vendored serviceNo perpetual dataSync FGS, boot receiver, or indefinite wake lock. Foreground multiplex SSE is a thread, not an Android ForegroundServiceNo perpetual background FGS for launch
Boot receiverRemovedNo automatic FGS restart from boot or package replace
Android pushFCM service polls on event=message and event=poll_request; subscribes to ~poll + QR topicsFCM wake → authenticated poll
iOS (reference)Firebase topic subscribe + APNs; see iOS APNs delivery architectureShared ntfy / Firebase semantics
Poll recoveryAuthenticated ?poll=1&since= (or since=all when empty)provisioning protocol
Play forms / FCM hostDraft 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.xml
  • keryx/vendor/ntfy/android/src/main/kotlin/io/karsmiths/ntfy/NtfyFirebaseMessagingService.kt
  • keryx/vendor/ntfy/android/src/main/kotlin/io/karsmiths/ntfy/NtfySubscriptionManager.kt
  • keryx/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
  end

Primary path (background, screen off, killed)

  1. The client ntfy host is configured for upstream Firebase push (for example upstream-base-url or equivalent deployer settings owned by #119).
  2. 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.
  3. Keryx wakes, performs a bounded authenticated HTTPS poll (?poll=1&since=…) using the per-QR subscribe token from the provisioning protocol.
  4. 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 dataSync FGS with START_STICKY and indefinite PARTIAL_WAKE_LOCK.
  • Automatic FGS restart from BOOT_COMPLETED, LOCKED_BOOT_COMPLETED, or MY_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

AlternativeWhy 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 pathToo slow and opportunistic for administrator alerts; may be a non-primary supplement only if implementation needs it
UnifiedPush distributor modeOut of scope for a first-party Google Play administrator companion

Fallback tiers

TierTriggerBehaviorLimitation
1FCM poll_requestAuthenticated poll + local notifyRequires Google Play services, deployer FCM/upstream config, and accepts FCM latency
2App foreground / resumeExisting poll-on-resume / startup pollNo delivery while killed without Tier 1
3Documented Tier 2 OEM battery exception (P0-04)Unrestricted battery or OEM-specific settings with client-IT approvalDevice-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_request semantics 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_SYNC or a dataSync service 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

RiskResidual after this design is implementedStill open until
Android 15+ dataSync timeout / Play rejection of perpetual FGSRemoved by deprecating perpetual FGSImplementation removes FGS/boot restart
Missed alerts without FCM/upstreamTier 2 only#119 deployer config + implementation
FCM latency or Play-services absenceDelayed or no background wakeSupport claims (safety-claims.md); Tier 1 OEM matrix (#117)
Force-stopNo restart until reopenDocumented limitation (safety-claims.md / #121)
OEM battery killersMay still block wakesP0-04 Tier 2 exceptions; #117 evidence
Unauthenticated pollSame as P1-02 residualP1-02 implementation

Closing #96 approves this architecture. It does not:

  • claim FCM, token registration, or FGS removal exist in code;
  • accept perpetual dataSync as launch-valid;
  • satisfy the P4-03 physical-device matrix or Play Console submission.

Implementation handoffs

WorkOwner
Remove/minimize FGS, change boot/package-replace behavior, add Android FCM/poll_request handlingLater 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_request hooks, docs/mobile/keryx.md, Android 15 FGS timeout guidance, Play FGS declaration policy.