A Mobile Device Management (MDM) system built with Kotlin Multiplatform. A server holds the desired policy for each device; an Android agent — running as a Device Owner — pulls that policy, enforces it on the device, and reports back what it actually observes; a Compose Multiplatform console (desktop + web) views compliance and edits policy. Every part speaks one shared, compiler-checked wire contract, so they can't disagree about the data on the wire.
All code lives under the package com.arganaemre.mdmcore.
The whole system is one feedback loop:
enroll → pull desired policy → enforce → report actual state → server detects drift
The server is authoritative: it recomputes compliance from the device's reported state rather
than trusting the agent's opinion, and a versioned policy lets it tell "the device hasn't caught up
yet" (STALE) apart from "the device has drifted" (NON_COMPLIANT). A policy change rings an FCM
doorbell — a data-only push that just wakes the device to sync; the policy itself always moves over
HTTP. Full walkthrough: docs/mdm-loop.md.
Five Gradle modules in a star, all pointing at the shared contract :protocol:
flowchart TD
desktop[":app:desktopApp"] --> shared[":app:shared"]
web[":app:webApp"] --> shared
shared --> core[":core"]
shared --> protocol[":protocol"]
server[":server"] --> core
server --> protocol
agent[":agent"] --> protocol
style protocol fill:#cde4ff,stroke:#4a90d9,stroke-width:3px
| Module | Role |
|---|---|
:protocol |
The contract — every @Serializable wire type, the compliance rule, the JSON config. Pure KMP. |
:server |
The brain — Ktor (Netty) on port 8081; holds desired policy + reports; sends FCM wakes. |
:agent |
The Android Device Owner / DPC — enforces policy via DevicePolicyManager, reports back. |
:app:shared + :app:desktopApp / :app:webApp |
The console — Compose Multiplatform UI. |
:core |
Non-wire shared domain logic. |
(app/iosApp is an Xcode project, parked as a viewer.) Details:
docs/architecture.md.
# Server (embedded Netty on http://localhost:8081)
./gradlew :server:run
# Desktop console (from a checkout not holding the Gradle lock)
./gradlew :app:desktopApp:run
# Android agent: build, install, then make it Device Owner on a fresh
# Google APIs emulator with no account signed in
./gradlew :agent:assembleDebug
adb install -r agent/build/outputs/apk/debug/agent-debug.apk
adb shell dpm set-device-owner com.arganaemre.mdmcore/.MdmDeviceAdminReceiverThen run a full enroll → edit → drift → re-comply cycle: see the end-to-end recipe. Build, run, and test commands for every module are in docs/development.md.
Gradle project lock:
:server:runand the console dev-runs are long-lived and hold the Gradle project lock, so any other./gradlewcommand blocks until they stop. Build artifacts first, then drive the rest withadb/curl. See docs/development.md.
Full docs live in docs/ (start here):
| Document | What it answers |
|---|---|
| architecture.md | The parts, the dependency star, the design principles. |
| mdm-loop.md | How desired policy becomes enforced reality, and how drift is detected. |
| protocol.md | The shared wire types, the compliance rule, the JSON config. |
| http-api.md | Every server endpoint, with example JSON and curl. |
| server.md · agent.md · console.md | Each component in depth. |
| fcm-doorbell.md | The push-wakes / HTTP-moves-data design. |
| firebase-setup.md | Service-account key + google-services.json setup. |
| development.md | Build, run, test; version catalog; end-to-end recipe. |
| troubleshooting.md | The failures you'll hit, by symptom. |
See also CLAUDE.md (guidance for AI contributors) and SOURCES.md (the
external docs the design was grounded in).
Kotlin Multiplatform targeting Android, JVM, JS, Wasm, and iOS · Ktor (server + client) · kotlinx-serialization · Compose Multiplatform · Firebase Cloud Messaging + WorkManager (agent) · Firebase Admin SDK (server). Pinned versions: docs/development.md#version-catalog.
This project is licensed under the MIT License — see LICENSE for details.