Appearance
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:
- Select friendly alert categories in SafeCall.
- Generate a QR code.
- Install Keryx and scan the code on the administrator's own device.
- 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.
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 applies to the complete English/Czech critical journey on physical devices: WCAG 2.2 AA, VoiceOver/TalkBack, meaningful spoken names, 200% text scaling, 4.5:1 normal-text and 3:1 large-text/UI contrast, 44×44 pt iOS and 48×48 dp Android targets, logical visible focus, keyboard/switch support where available, reduced motion, every supported orientation, and camera/notification Settings recovery without dead ends.
These are target requirements, not current conformance. The source still permits iOS 13 and Android API 24, has no complete semantics/accessibility evidence, lacks complete permission-denial/revocation recovery, and has no executed tablet/orientation or OEM matrix. Android and iOS delivery architectures are approved under P1-03/P1-04, but FCM/APNs implementation, signing entitlement, deployer upstream, and physical proof remain open.
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, P3-06, P4 support handbooks, P5 console checks, and P6-05 own those follow-ups.
Current Provisioning Implementation
- An admin opens the SafeCall dashboard.
- Mobile Access shows friendly SafeCall alert categories. The seven operational categories are selected by default.
- 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.
- Keryx scans the QR code, stores the server URL and selected topics locally, and starts receiving SafeCall alerts.
- 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-batteries"],
"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
}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 eight staff categories through GET /api/v1/features as ntfy_topics. The ntfy name is {server_id}-{topic_id}:
| Id | Admin label (EN) |
|---|---|
sensors | Sensor threshold alerts |
sos | Panic button alerts |
watches | Watch button alerts |
batteries | Low battery alerts |
notseen | Device not seen alerts |
calibration | Calibration expiry |
test | Notification pipeline tests |
health | Installation 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:
- Rectangular cards only for the alert categories included in the scanned QR
- 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 condition/device information and a settings shortcut. The open category screen rebuilds from local stores when a new message arrives; you do not need to leave and return to see status or history change.
- 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 - A bottom tab bar with
ActiveandArchivefor message history Active: unread notifications onlyArchive: notifications marked read or manually archived- A
Mark all notifications readaction that moves every active notification to the archive - A
Delete all archivedaction 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_lightdisplayed 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
Advancedsection for pausing/resuming notifications, scanning a new QR code, and resetting the app
Notification policy only changes OS pushes. Every received message is retained according to the normal history cap and still updates its category card. The first matching active/occurred alert is shown immediately; later arrivals for the same category/device are silent until the selected rolling window expires. Resolution and snapshot records update state silently. The test category has no cadence controls and always alerts when subscribed.
Reset App removes the QR configuration and local notification history. It does not change SafeCall server settings.
Notification Delivery
Keryx uses a vendored ntfy Flutter plugin path dependency for native subscriptions.
Current implementation:
- Android uses FCM wakes (
event=messageduring anonymous dual-run, orevent=poll_requestafter deny-all) plus authenticated HTTPS poll. There is no perpetualdataSyncforeground 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 alert-intent messages (
phaseactive or occurred) and for every test-category message. Snapshot, resolved, andintent=stateupdates stay silent and only refresh category cards. - On iOS, the vendored plugin is the
UNUserNotificationCenterdelegate after subscribe and askswillPresentfor banner, list, sound, and badge. That overrides the default (andflutter_local_notifications) behavior of hiding non-plugin locals while Keryx is open. Killed-state wake uses the Keryx Firebase project (FCM topic subscribe), notPUT /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-fileremains required for reliable killed-state delivery (#119, ntfy-fcm-apns.md). Placeholdergoogle-services.json/GoogleService-Info.plistcannot 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-QRAuthorizationheader from the provisioning protocol. A topic with no stored id usessince=allso a new phone can recover a daily condition snapshot still in the ntfy cache. - Generating a Keryx QR also publishes a silent current-state snapshot (
phase=snapshot,intent=state) for each stateful category in that QR (sensors, batteries, not-seen, calibration, health). That does not consume the scheduled 09:00 digest. - 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
~pollplus each QR topic. It does not call stock ntfy’s non-existent/v1/account/deviceAPI. - 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=messageAPNs alerts already show a lock-screen banner; Keryx polls to refresh cards and does not post a second local notification.poll_requestwakes still poll and apply the Swift policy gate. - 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 saydevelopment.
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). 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 per-category policy screens before turning on the new SafeCall alert record and digest cadence at a site. Older Keryx builds and stock ntfy continue to show readable titles and summaries. New Keryx treats messages without the v1 record as history only and shows Waiting for status until an explicit condition snapshot arrives. See SafeCall ntfy / Keryx alert topics.
Publishing
See Publishing Keryx for the first release, routine updates, hotfixes, store review, rollout, and lifecycle runbook.
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