Skip to content

Architecture

AstorisTheBrave edited this page Jul 5, 2026 · 3 revisions

Architecture

URSA is a thin, reactive client. A network layer parses Uptime Kuma's wire formats into clean domain models, repositories expose those as flows, and Compose renders them. Nothing is stored server-side, and the app holds as little as possible.

Data flow

Uptime Kuma  --Socket.IO-->  core.network (adapter: parse/normalize wire -> domain)
             --REST (Ktor)->        |
                                     v
                             data.repository (StateFlow<DomainModel>, single source of truth)
                                     v
                             ui.* (Compose, collectAsStateWithLifecycle)

core.storage (DataStore + Tink)  --credentials/JWT-->  core.network (login/loginByToken)
distributor (ntfy)  --push-->  core.push (UnifiedPush service)  -->  system notification

Layers

  • core.network - one Socket.IO connection per active server (auth, live monitor status, heartbeats, certificates, pause/resume) plus a Ktor REST client for public status pages. All of Kuma's wire quirks are isolated here. See Network and Protocol.
  • core.storage - DataStore Preferences with Tink AES-256-GCM encryption at rest; the master key lives in the Android Keystore. Only the session token is persisted.
  • core.push - the UnifiedPush receive path and notification rendering, including the down-to-up transition tracking that reports how long a monitor was down. See Push Internals.
  • core.work - WorkManager jobs and notifiers that run without the UI: the daily TLS-expiry reminder and the periodic slow-response check (which briefly connects to each saved server, reads the latest heartbeats, and alerts on monitors that are up but over a response-time threshold). The threshold evaluator is a pure, unit-tested object.
  • data.model - the domain types (monitors, heartbeats, certificates, status pages).
  • data.repository - owns the active network client and exposes StateFlows as the single source of truth.
  • ui - one UrsaViewModel; a bottom-navigation shell (MainShell) with Monitors, Notifications, and Settings tabs, and a top-level when in UrsaApp that picks the screen from state (no navigation library for the current, small screen set). System back is handled per screen. Service favicons are fetched best-effort by FaviconCache.

The :wear module

The Wear OS companion is a separate Gradle module (:wear) that shares the app's applicationId. It is deliberately free of Google Play Services: instead of the proprietary Wearable Data Layer, the watch tile (StatusTileService, built with androidx tiles + protolayout) polls a public Kuma status page directly, and Wear OS routes that network call through the phone. A small config Activity stores the status-page URL on the watch. Notification and action bridging to the watch is handled by the Wear OS system with no code. See Wear OS.

Principles

  • Verify before you model. Uptime Kuma's socket API is internal and unstable, so every wire shape URSA relies on was captured from a live instance.
  • One adapter owns the quirks. Positional socket events, snake_case vs camelCase fields, and double-encoded JSON are all handled in the network layer and nowhere else.
  • State-driven UI. Routing is a function of ViewModel state. Reconnects keep the list visible with a status banner rather than bouncing to the login screen.
  • Kuma-matched theme. The Material 3 theme (ui/theme) uses Uptime Kuma's own palette (from its vars.scss) for light and dark mode by default, with an opt-in Material You mode. Status colors always stay Kuma-semantic (they come from StatusUi, not the scheme), so up/down/pending/maintenance read the same regardless of theme.

For the code map and deeper notes, see the docs/ folder in the repository.

Clone this wiki locally