Skip to content

Architecture

yybam edited this page Oct 7, 2026 · 1 revision

Architecture

CORE MDM has three parts: the web console, Firebase (Firestore + Auth), and the Android app. There is no always-on custom server in the command path — Firestore's real-time listeners are the transport.

Browser (coremdm.web.app)
    │  signs in (Firebase Auth) and writes Firestore documents / queues commands
    ▼
Firebase Firestore   /devices/{deviceId}   (+ /devices/{deviceId}/commands)
    │  real-time snapshot listeners
    ▼
MdmCommandService  (Android foreground service)
    │  claims & executes commands, applies policies
    ▼
DevicePolicyHelper → DevicePolicyManager (Android OS)

Components

Web console (public/index.html)

A single-file vanilla-JS SPA using the Firebase JS SDK (v10.12.2). It authenticates the admin, lists the devices they own (where ownerId == uid), and writes policy fields / queues commands directly to Firestore. No build step.

Firebase

  • Auth — identifies both the admin (in the browser) and the device (in the app). Email/password and Google.
  • Firestore — the single source of truth. Each device is a document at /devices/{hardwareId}; commands live in a commands subcollection. See Firestore Data Model.
  • Security rules (firestore.rules) enforce per-user ownership and let a device update only its own record. See Security Rules.

Android app

  • MdmCommandService — a foreground service that holds the live Firestore listeners, applies policies, and runs queued commands. It re-attaches automatically if a listener drops, and a 60-second heartbeat updates lastSeen and re-enrolls if needed.
  • EnrollmentManager — derives the stable hardware ID and writes/updates the device document on sign-in.
  • DeviceRegistry — all Firestore reads/writes: the device-doc listener, the command-queue listener, claimCommand (transaction), the installed-app inventory sync.
  • DevicePolicyHelper — a thin wrapper over Android's DevicePolicyManager; every restriction/command goes through here.
  • AppPolicyManager — per-app hide/suspend.
  • DnsVpnService — the DNS content filter VPN.
  • PinManager — the PBKDF2-hashed in-app PIN.
  • Jetpack Compose UI: dashboard, login, PIN, apps, kiosk, filter, provisioning, telemetry.

Two interaction patterns

Commands — queued, one-shot, cancellable

  1. Console adds a doc to /devices/{id}/commands: { type, state: "pending", createdAt }.
  2. The device's queue listener sees it and calls claimCommand, a Firestore transaction that flips pending → executed only if it's still pending.
  3. If the transaction wins, the device runs the command (lock / reboot / wipe / lockdown).
  4. If the admin pressed Cancel first, the state is already cancelled, the transaction aborts, and the device does nothing.

This guarantees a command runs at most once and can be cancelled in the window before pickup.

Policies — persistent, declarative

The console writes policies.<key> on the device doc. The device's doc listener diffs the policy map and applies only what changed, then re-asserts it on reconnect and on boot (BootReceiver). The Firestore field is the desired state; the device converges to it.

Why Firestore instead of a server

Firestore's snapshot listeners give sub-second push to the device over its existing authenticated connection, survive network changes, and queue writes while offline — so a command sent to an offline phone is delivered when it reconnects, with no custom infrastructure. The optional backend/ (Node) and admin-backend/ (Spring Boot) services exist for extras like WebRTC screen control, not for the core command path.

Resilience

  • Listener re-attach: if a Firestore listener errors, the service clears it and the heartbeat re-creates it.
  • Boot persistence: BootReceiver restarts the service and re-applies baseline restrictions, status-bar state, and uninstall protection.
  • Always-on content filter: on Device Owner, the VPN is registered as always-on, so it restarts itself after reboot.

Clone this wiki locally