Skip to content

Keryx for SafeCall Notification App ​

keryx/ contains Keryx for SafeCall, SafeCall's first-party companion Flutter app for administrators to receive building safety notifications after scanning a QR code from their own SafeCall installation. It replaces the generic ntfy app in the supported client workflow with SafeCall-controlled branding, provisioning, notification UX, and support.

Keryx is not a standalone notification product. Public stores are an installation channel for contracted SafeCall clients, not a self-service acquisition channel.

Product Model ​

The intended administrator experience is:

  1. Select friendly alert categories in SafeCall.
  2. Generate a QR code.
  3. Install Keryx and scan the code on the administrator's own device.
  4. Verify a test notification, review category cards, and tune alert cadence.

The QR can contain ntfy transport configuration, but ordinary administrators must not need to understand or manually enter server URLs, topics, or subscriptions. Those details belong to the implementation and restricted support diagnostics.

App Identity ​

  • Project path: keryx/
  • Public store name: Keryx for SafeCall
  • In-app product name: Keryx for SafeCall
  • iOS/Android launcher label: Keryx
  • Legal publisher, seller, and intended Apple/Google account owner: sourcectl
  • Copyright: Copyright © 2026 sourcectl
  • Android application ID: com.sourcectl.keryx
  • iOS bundle ID: com.sourcectl.keryxapp

Roadmap P0-01 approves this target identity. The iOS bundle ID was amended 29 August 2026 after Apple would not release com.sourcectl.keryx (case 102915938031). The current source has not yet fully propagated launcher naming: iOS and Android use lowercase keryx display labels, while the Flutter application and app bar use SafeCall. P2/P3 own metadata propagation and external account verification; this decision does not make those surfaces release-ready.

Launch Classification and Localization ​

Roadmap P0-02 approves:

  • Czech Republic-only availability on Apple App Store and Google Play;
  • English as the primary/default store locale plus complete Czech localization across Keryx, SafeCall administrator provisioning, listings, screenshots, release notes, relevant permission/help copy, and install/support material;
  • Business categories on both stores for SafeCall administrators aged 18 or older, with no child targeting or kids/families participation;
  • expected questionnaire results of Apple 4+ and Google Play/IARC Everyone;
  • no health functionality and a “not a regulated medical device” answer.

These are target requirements, not current behavior. P2-05 #108 implements English/Czech parity in source per localization policy. Platform permission strings, Flutter ARB catalogs, and SafeCall admin provisioning catalogs are wired; live Czech HTTPS pages and store paste remain later items. Keryx Settings lets the administrator pick English, Čeština, or System default (device locale). OS permission strings stay on the phone language.

The final store ratings remain questionnaire-derived. A higher result must reopen P0-02 rather than being suppressed through inaccurate answers. P1-10 #103 owns final safety and delivery-limitation claims.

Commercial and Managed Deployment Model ​

Keryx for SafeCall is a free companion for administrators of a separately contracted SafeCall physical/building deployment. Downloading Keryx does not buy or provide SafeCall. The app has no Keryx user account, checkout, subscription, paid SKU, in-app purchase, external purchase link, or purchase call to action.

The SafeCall-generated QR carries connectivity settings and the administrator's selected alert categories for their deployment. Scanning it configures notification delivery; it does not purchase, subscribe to, license, or unlock paid digital content. Without compatible SafeCall configuration, Keryx remains unconfigured.

Clients may assign or install the same public store apps (com.sourcectl.keryxapp on Apple Business Manager; com.sourcectl.keryx on managed Google Play) through Apple Business Manager/standard MDM or managed Google Play. MDM is an installation channel only. Keryx does not currently implement managed app configuration, MDM-injected credentials, deep-link provisioning, zero-touch setup, custom/private enterprise binaries, or silent configuration. Administrators must grant required permissions and scan their SafeCall-generated QR on the device.

This does not claim compatibility with every restrictive managed-device profile. Client IT must allow the required camera, notification, network, and background behavior, and the approved P0-04/P4 matrix must verify supported conditions. Current first-launch copy also does not yet explain the commercial dependency and refers users to a separate SafeCall administrator; P2-05 #108 must align the bilingual runtime wording.

Support Baseline ​

Roadmap P0-04 approves:

  • iOS/iPadOS 17+ on iPhone/iPad, tested on versions 17, 18, and 26;
  • Android 12/API 31+ through Android 16/API 36 on Google Play-certified phones and tablets, with API 36 target/compile and release-AAB 16 KB native-page verification;
  • iPhone portrait and landscape left/right, iPad all four orientations, and Android phone/tablet portrait and landscape;
  • a functioning camera for QR provisioning.

Foldables and Chromebooks are not separately certified. macOS, Windows, Linux, web, visionOS, wearables, Android Auto, CarPlay, Fire OS, devices without supported Google Play distribution, and devices without a functioning camera are outside launch support.

Android support is tiered:

  • Tier 1: Google Pixel/AOSP baseline and Samsung Galaxy; the complete physical delivery, lifecycle, orientation, tablet, and accessibility matrix must pass.
  • Tier 2: only tested Xiaomi/Redmi/POCO, Motorola/Lenovo, and OnePlus/Oppo/Realme model/OS combinations whose documented, least-privilege battery/background settings pass with client-IT approval.
  • Tier 3: no-GMS Huawei, Fire OS, unknown/uncertified OEMs, rooted/custom ROMs, EOL devices/OSes, and restrictive profiles that block required capabilities remain unsupported until qualified.

Force-stop/force-quit is a documented platform limitation. A Tier 1 device must not silently depend on undocumented battery exceptions, and a Tier 2 model is removed from support when its approved settings cannot pass.

The launch accessibility gate is not a WCAG 2.2 AA or VoiceOver/TalkBack pass. Keryx is for named hospital security personnel and SafeCall administrators. In-repo camera/notification Settings recovery exists. #116 closed 20 September 2026. Do not claim current conformance.

These are target requirements, not current conformance. The source still permits iOS 13 and Android API 24, and has no executed tablet/orientation or OEM matrix. Android and iOS delivery architectures are approved under P1-03/P1-04; physical OEM/soak proof remains #117.

Launch Ownership and Support ​

Roadmap P0-05 approves:

  • Thomas Minitsios as go/no-go authority and backup, and as Responsible and Accountable owner for product, app, SafeCall, ntfy/push, security/privacy, legal, stores, content, support, and incidents;
  • a named single-person concentration risk, reviewed when account recovery is verified and at the first P5 go/no-go;
  • no invented beta or production calendar date until remaining P1–P4 launch blockers are closed or explicitly accepted;
  • the SafeCall support channel (no standalone Keryx mailbox), Czech business hours Monday–Friday CET/CEST, next-business-day first response, and same-business-day acknowledgment of supported-tier core-path incidents by the go/no-go owner;
  • no public status page, 24/7 desk, or emergency-dispatch SLA;
  • no analytics or crash SDK at launch; field diagnosis is client-reported until a later privacy-approved minimum signal exists.

These are operating decisions, not completed artifacts. Public support, privacy, and terms URLs, monitored mailbox evidence, and an intake process are still missing. P1-06, P2-04, P5 console checks, and P6-05 own those follow-ups. The English administrator how-to is admin-provisioning-handbook.md (#120 closed 13 September 2026). Czech handbook stays on #108. The English phone-side how-to is app-onboarding-guide.md (#121; live web, Czech, screenshots, and OA-121-app-walk remain).

Current Provisioning Implementation ​

  1. An admin opens the SafeCall dashboard.
  2. Mobile Access shows friendly SafeCall alert categories. SOS, watches, and daily status are selected by default. Condition categories stay available but are unchecked because Overview loads them from the status API.
  3. The admin selects the alert types and generates a Keryx QR code. Pipeline tests and debug access are unchecked under collapsed Admin features; if either is enabled, a warning must be confirmed before generation.
  4. Keryx scans the QR code, stores the server URL, status URL, and selected topics locally, and starts receiving immediate wakes. Existing phones need a new QR for {server_id}-status and status_url.
  5. After the first scan, the administrator sees a notification list. Technical details such as topics, server URL, and stream status are hidden from the default UI.

The same administrator is the intended QR recipient; the supported model is not an administrator generating a code for a separate generic end-user audience. Topic names and subscribe credentials remain implementation details.

QR Payload ​

The dashboard mints a versioned JSON payload via POST /api/v1/keryx/qr (the SPA never mints subscribe tokens in the browser):

json
{
  "type": "safecall.keryx.ntfy.v2",
  "ntfy_server_url": "https://notify.example/",
  "topics": ["TST000-sos", "TST000-watches", "TST000-status"],
  "labels": {
    "TST000-sos": "Panic button alerts"
  },
  "subscribe_auth": "Bearer tk_example_subscribe_only",
  "token_id": "optional-support-correlation-id",
  "created_at": "2026-08-26T12:00:00.000Z",
  "debug_access": false,
  "status_url": "https://notify.example/keryx-status"
}

Keryx accepts only safecall.keryx.ntfy.v2 with a required subscribe_auth and an HTTPS ntfy_server_url. It rejects v1 scans and refuses stored v1/HTTP configs on load. SafeCall publishes with a separate server-side publish token that never enters the QR. When the ntfy host has no /v1/account/token API yet (open/anonymous host, before #119 ACL), SafeCall still issues a local subscribe_auth so the QR stays scannable. ACL hosts must set server_ntfy_token_manager_user and server_ntfy_token_manager_password. See authenticated provisioning. Operator steps for the interim dual-run (auth on, official ntfy app still anonymous, then Play Internal): ntfy auth dual-run and Play Internal.

The optional debug_access field defaults to false. When enabled by the dashboard QR generator, restricted diagnostics can show topic names, ntfy message IDs, sent times, priority, tags, and the raw notification JSON. Standard QR codes only expose the notification message and received time on the detail screen. Debug access is not part of ordinary administrator setup. The subscribe token must never appear in ordinary or debug UI.

Internal Alert-Category Mapping ​

The full topic inventory (triggers, tags, priority, cooldown subjects, and repeat behavior) is SafeCall ntfy / Keryx alert topics.

SafeCall exposes nine staff categories through GET /api/v1/features as ntfy_topics. The ntfy name is {server_id}-{topic_id}:

IdAdmin label (EN)
sosPanic button alerts
watchesWatch button alerts
statusDaily status
sensorsSensor threshold alerts
batteriesLow battery alerts
notseenDevice not seen alerts
calibrationCalibration expiry
testNotification pipeline tests
healthInstallation health alerts

These IDs and generated names are implementation details. The administrator-facing SafeCall UI must use friendly category labels such as “Panic button alerts” and carry the internal mapping in the generated QR.

Current App Experience ​

Unconfigured administrators currently see:

  • Title: No notifications have been enabled yet
  • Help text currently directing them to ask a SafeCall admin for a QR code
  • Primary action: Scan QR code

That help text predates the administrator-only product decision. It must direct the administrator to generate the QR from their own SafeCall installation and must not imply a separate end-user provisioning model.

Configured administrators see:

  • Overview cards for SOS/watch/test (if subscribed) plus every unmuted condition category. Daily status is a wake, not a card. Condition counts come from GET keryx-status, not stacked ntfy history.
  • Category-specific icons and status: Waiting for status, All clear as of …, or the current active-condition count; SOS/watch/test cards show recent moments instead of claiming an OK state
  • A category detail view with items from the status API and a settings shortcut. Pull-to-refresh reloads the snapshot.
  • A category policy that defaults to Respect SafeCall, with 1 h, 6 h, 12 h, 24 h, and muted choices; known devices inherit that policy or have an individual override. Muting Daily status (or all five condition categories) suppresses the daily OS banner. Per-device mute filters Overview detail.
  • A bottom tab bar with Active and Archive for SOS, watch, and test moments
  • Active: unread SOS/watch/test notifications only
  • Archive: those moments marked read or manually archived
  • Archive filters: category chips, fuzzy text search, and an inclusive local date range
  • A Mark all notifications read action that moves every active notification to the archive
  • A Delete all archived action that hides archived notifications while retaining local tombstones so cached ntfy messages are not reintroduced as active
  • Per-notification actions to view details, archive, or delete
  • ntfy emoji tags in notification cards and OS notifications, such as rotating_light displayed as 🚨
  • An app icon badge count that reflects only active notifications; the badge is cleared while notifications are paused and skipped on Android launchers that do not support numeric badges
  • A settings screen with an Advanced section for the on-device delivery log (wakes, banners, notify-affecting policy; Clear log empties the inspector only), pausing/resuming notifications, scanning a new QR code, and resetting the app

Notification policy only changes OS pushes for SOS, watch, test, and the daily status banner. Condition topics never raise an OS banner. Active is moments only. The test category has no cadence controls and always alerts when subscribed.

Scanning a new QR (or Reset) clears local history, category state, delivered OS notifications, and native inbox. It does not require a separate Reset. Overview stays on Waiting for status until GET keryx-status succeeds. Existing phones need a new QR for status_url and {server_id}-status. If the QR used a local tk_local_ subscribe token against an ACL notify host, HTTP poll/SSE fail while FCM/APNs banners can still appear; generate a new QR after token-manager mint is configured.

Reset App removes the QR configuration and local notification history. It does not change SafeCall server settings.

Use this when a lock-screen or tray banner needs a clean inspector dump. Clear log on the phone empties the inspector ring only. It does not reset the QR, seen ids, or the OS tray (resetting those would restorm banners).

  1. Admin: Settings → Notifications → Clear log, then reproduce.
  2. Keryx: Settings → Advanced → Delivery log → Clear log.
  3. Background or lock the phone, record the lock screen, then Copy log on both sides (Keryx delivery log and SafeCall publish log) and paste.

Match ntfy ids between the two copies. The phone log is wakes, posted or skipped banners, and policy that affects notify — not ntfy cache replay.

Notification Delivery ​

Keryx uses a vendored ntfy Flutter plugin path dependency for native subscriptions.

Current implementation:

  • Android uses FCM wakes (event=message during anonymous dual-run, or event=poll_request after deny-all) plus authenticated HTTPS poll. There is no perpetual dataSync foreground service, boot FGS restart, or indefinite wake lock in the vendored plugin.
  • While the app is open, one comma-separated multi-topic JSON stream covers all QR categories without a sticky FGS. Selecting eight categories therefore uses one persistent SSE connection, not eight.
  • Foreground OS banners are presented for new SOS, watch, and test moments, plus the daily status wake. Condition topics never banner. Resume, lock-screen tap, and poll-on-open catch-up ingest history and Overview cards but do not re-post OS notifications. Unpausing in Settings can still banner ids the native policy gate has never seen. Resolved and intent=state updates stay silent and only refresh category cards. FCM/APNs event=message is ingested from the payload. poll_request HTTP-polls ingest-only; if the wake is generic (no usable ntfy message id) Keryx posts at most one local for the newest unseen alert-intent message. Tap extras include the message body so Active history can open without a successful poll. Unknown/poll APNs ids open the category card or Active instead of failing. Settings → Advanced has a redacted on-device delivery log (OS wakes, posted or ingest-only-skipped locals, notify-affecting policy, taps). Catch-up polls log one catch_up fetched=N added=M line instead of every cached ntfy id. Clear log empties the inspector ring only; it does not reset QR, seen ids, or the OS tray. SafeCall Settings → Notifications can Clear and Copy the ntfy publish FIFO so a debug session starts empty on both sides.
  • On iOS, the vendored plugin is the UNUserNotificationCenter delegate after subscribe and asks willPresent for banner, list, sound, and badge only for Keryx-built local notifications. Remote APNs banners stay hidden while the app is open so the same ntfy id is not shown twice. That still overrides flutter_local_notifications hiding ntfy locals while Keryx is open. Killed-state wake uses the Keryx Firebase project (FCM topic subscribe), not PUT /v1/account/device.
  • Kotlin is the only Android OS-notification path. It reads the policy snapshot written by Dart, owns rolling-window timestamps/dedupe state, and forwards every message to Dart even when the lock-screen notification is suppressed.
  • Deployer Firebase + notify-host firebase-key-file is required for reliable killed-state delivery. Client files for keryx-for-safecall are in repo; host setup and first-device proof are done (#119, ntfy-fcm-apns.md, ntfy-deployer.md). A binary built without those client files cannot receive production FCM.
  • Some Android OEM battery managers can still delay background wakes.

Approved architecture: Android delivery architecture (P1-03). Without deployer FCM/upstream configuration, Android falls back to poll-on-resume only.

Shared behavior:

  • On startup and app resume, keryx polls ntfy's cached message API with ?poll=1&since={lastMessageId} and the per-QR Authorization header from the provisioning protocol. A topic with no stored id uses since=all. Condition and daily-status messages still advance the cursor so they are not refetched, but they do not enter Active history. Overview loads from GET keryx-status on scan, resume, and pull-to-refresh.
  • Generating a Keryx QR PUTs the current condition snapshot to keryx-status. It does not POST silent 3 KiB×N ntfy snapshots.
  • Recent active and archived alerts are stored locally on the device, capped to the latest 200 visible messages. Deleted notifications leave small local tombstones so recovered cached alerts are not reintroduced as active notifications.
  • Dart stores category/device preferences and authoritative SafeCall condition state separately from the Active/Archive history. Legacy messages remain readable history but do not turn a category card green.
  • SafeCall v1 alert records distinguish moment events from active, resolved, and snapshot condition updates. The human title/summary remains readable in stock ntfy.

ntfy's cache is time-limited by server configuration. keryx can recover messages still present in that cache, but it does not provide unlimited notification history.

Force-stop, Do Not Disturb / Focus, OEM battery managers, and missing upstream push remain honest delivery limits for support and store claims (#103).

iOS Push Prerequisite ​

Current implementation:

  • Keryx registers for remote notifications and hands the APNs token to Firebase Messaging, then subscribes to FCM topics ~poll plus each QR topic. It does not call stock ntfy’s non-existent /v1/account/device API.
  • Official ntfy self-host setup (upstream-base-url: https://ntfy.sh, Instant Notifications) wakes the official ntfy iOS app only. It does not wake Keryx (com.sourcectl.keryxapp).
  • Dual-run event=message APNs alerts already show a lock-screen banner; Keryx polls to refresh cards and does not post a second local notification. poll_request wakes poll ingest-only and may post one replaceable local for the newest unseen SOS/watch/test message. The daily {server_id}-status wake uses Firebase: yes so a closed app can open Overview. Condition topics never raise OS banners. SafeCall Settings → Notifications lists recent ntfy publish ids for support correlation; Copy log dumps the full FIFO and Clear log empties it for a debug session.
  • Swift is the only iOS OS-notification path for locally built banners and applies the same category, device, rolling-window, test-exemption, and state-only rules as Android. While Keryx is in the foreground it presents those local notifications as banners.
  • Signed TestFlight / store IPAs prove aps-environment=production (#110). Source entitlements may still say development.

Approved architecture: iOS APNs delivery architecture (P1-04). The APNs-capable upstream is the Keryx Firebase project on the sourcectl notify host (firebase-key-file), not ntfy.sh. Operator steps: ntfy-fcm-apns.md (OA-110-apns-p8, OA-119-firebase, OA-119-ntfy-apns, OA-119-ntfy-fcm). Deployer packet: ntfy-deployer.md. Without that deployer path, iOS is stream-while-alive plus poll-on-resume only. Force-quit, Focus/DND, cache expiry, and sandbox/production entitlement mismatch remain documented failure modes.

SafeCall still publishes alerts to ntfy topics; no SafeCall APNs token API is part of this design.

Implementation Note ​

Keryx keeps QR parsing, alert state, message history, and policy in app source. NtfyStreamService wraps one multiplexed native stream. Dart synchronizes immutable user policy to native code; native code owns mutable delivery-window state so killed/background delivery cannot bypass a preference. Cached-message recovery uses the same native policy gate and Dart deduplication.

Local data, retention, and lock-screen ​

Approved launch policy is in data classification and local storage (P1-05):

  • Config and alert history (including subscribe_auth) may live in SharedPreferences; treat subscribe credentials as secrets in docs and support.
  • Alert state, known device labels/identifiers, per-category/per-device policy, and native last-notified timestamps are also local application data and are cleared on reset/new provisioning.
  • OS notifications may show full title and body on the lock screen; privacy and store forms must disclose that exposure.
  • Visible history caps at the latest 200 messages; deleted items leave up to 500 tombstones so ntfy cache polls do not reintroduce them.
  • Reset App clears local stores only; offboarding still requires ntfy token revoke. Device backups may contain alert history and config secrets.

Closing the policy does not change current storage or notification builders.

Privacy, terms, and support (draft) ​

English DRAFT privacy, terms/EULA, and support commitment texts live in docs/gtm/keryx/legal/. They are not counsel-reviewed and are not live public pages yet. Planned public paths after #107:

  • {public_base}/keryx/privacy
  • {public_base}/keryx/terms
  • {public_base}/keryx/support

{public_base} and monitored contacts remain in controlled records. In-app Settings/About links wait until those URLs are published.

Compatibility rollout ​

Deploy the Keryx dual parser and Overview GET before turning on the daily status plane at a site. Older Keryx builds and stock ntfy continue to show readable SOS/watch/test titles. New Keryx ignores condition ntfy in Active and shows Waiting for status until GET keryx-status succeeds. Existing phones need a new QR. See SafeCall ntfy / Keryx alert topics.

Publishing ​

See Publishing Keryx for the first release, routine updates, hotfixes, store review, rollout, and lifecycle runbook.

Administrator first-line how-tos: Administrator provisioning handbook (Mobile Access QR; #120) and Administrator app onboarding guide (phone-side; #121). Deployer/host work stays on the restricted ntfy-deployer.md packet.

The Keryx adoption strategy defines the fixed SafeCall companion model, administrator audience, and rollout approach. Operational readiness is tracked by the Keryx go-to-market roadmap and GitLab parent work item #81. The roadmap is the source of truth for unresolved decisions, gaps, risks, and go/no-go gates; the current application documentation does not imply store readiness.

Validation ​

bash
bun analyze:keryx
bun test:keryx
bun build:keryx

Signed AAB/IPA from bun keryx / bun run release:keryx land in releases/keryx.aab and releases/Keryx.ipa.