Magnemite

How this magnet works

Four processes, two domains, and the reasons each one is separate.

Magnemite is four processes and a database.
Three of them run on your local server; the fourth is a ~6 MB static binary on each box.

All the pieces

PieceWhat it isWhat it does
agentGo binary, installed as a Magisk moduleDials out to the hub over WSS, downloads bundles, drives pm
hubFastify, port 3001Holds the device sockets, schedules jobs, polls both release sources, caches each .apkm once
webNext.js, port 3000The dashboard. Reads the database directly; anything touching a live socket goes through the hub's internal API
edgeCaddyFronts the hub and streams cached bundles straight off the shared volume
databaseMySQL 8, MariaDB 11 or Postgres 17Devices, versions, rollouts, jobs, accounts

Magnemite ships no database service

It connects to one you already run, over DATABASE_URL. DB_PROVIDER (mysql, which also covers MariaDB, or postgresql) must match that URL, and changing it needs a rebuild, the Prisma client is generated per provider.

                         ┌──────────┐
  you ───────────────────►   web    │──────► database
       dashboard domain  └────┬─────┘           ▲
                              │ /internal/*     │
                         ┌────▼─────┐           │
  box ──── WSS ─────────►│   hub    ├───────────┘
       agents domain     └────┬─────┘
                              │ writes artifacts
                         ┌────▼─────┐
  box ──── /files/* ────►│   edge   │  (reads the same volume, read-only)
                         └──────────┘

Why there is an edge at all?

The hub can serve /files/* itself (SERVE_ARTIFACTS=true), and in local development it does. In production it does not, because a fleet-wide rollout is ~35 GB of .apkm bytes and there is no reason for Node to touch them.

SERVE_ARTIFACTS is false in both deployments.

Caddy mounts the artifacts volume read-only, answers Range requests natively — so a box that drops its uplink at 80% resumes instead of starting over — and asks the hub whether the requesting device's token is good via forward_auth against /internal/authz.

Two URLs, two audiences

Two different things talk to Magnemite, and it matters which URL each one gets.

AudienceWhat it needsEnv var
You, in a browserThe dashboard: login, fleet, rolloutsMAGNEMITE_DASHBOARD_URL
The boxesThe hub: /ws/device, /api/enroll, /files/*MAGNEMITE_PUBLIC_URL

Everywhere a box is configured, the value is the hub URL

SERVER= when building the module, serverUrl in config.json, -server on the agent: all of them are what MAGNEMITE_PUBLIC_URL is set to, never the dashboard's domain. The agent builds its artifact download URLs from it, and a box pointed at the dashboard fails the handshake on /ws/device.

A. With docker compose both live behind one virtual host on your proxy, so the two URLs are identical and compose fills the dashboard one in for you. Set MAGNEMITE_PUBLIC_URL and you are done.

B. With Coolify they are split across two domains — the dashboard on magnemite.example.com, the boxes on agents.magnemite.example.com.

What the boxes can reach

A box asks for exactly four paths, and the edge routes exactly those plus a little for its own deployment. Nothing routes /internal/*, which is what keeps the hub's internal API unreachable from outside — the protection is the absence of a route, not a rule.

PathHandled byWhat asks for it
/ws/devicehubThe persistent WebSocket every device opens and keeps
/api/enrollhubFirst boot, trading an enrollment token for a device token
/files/*edgeCached .apkm downloads, gated by forward_auth against the hub
/api/logs/*hubThe log bundle a box uploads when the dashboard asks for one
/api/screenshots/*hubThe screen capture a box uploads, too big for a socket frame

The two edge configs are not the same file

deploy/edge/Caddyfile fronts one virtual host, so after those five it falls through to the dashboard on web:3000 — nothing 404s there, and it also routes /api/events* to the hub. deploy/edge/Caddyfile.coolify fronts the agents' domain only: it adds /healthz on the hub and answers everything else with a flat 404, because the dashboard lives on its own domain. So /healthz reaches the hub under Coolify and the dashboard under compose, and a probe written against one deployment measures something else in the other.

The hub's internal API

Reachable only from the web container over the compose network, behind HUB_INTERNAL_SECRET. It is the whole set of operations that need a live socket or the scheduler.

# the hub itself
GET  /internal/status                      GET  /internal/events               (SSE)
POST /internal/health                      POST /internal/nudge
POST /internal/settings                    POST /internal/tracked-packages/refresh

# rollouts and their jobs
POST /internal/rollouts                    POST /internal/rollouts/:id/cancel
POST /internal/rollouts/:id/resume         POST /internal/rollouts/:id/retry-failed
POST /internal/jobs/:id/retry              POST /internal/jobs/:id/cancel

# one device, over its live socket
POST /internal/devices/:id/reboot          POST /internal/devices/:id/agent-update
POST /internal/devices/:id/exec            POST /internal/devices/:id/logs
GET  /internal/devices/:id/logs/:bundleId  GET  /internal/devices/:id/logs/live
POST /internal/devices/:id/rotom/:action   POST /internal/devices/:id/rotom/workers
POST /internal/rotom/sync

# builds
POST /internal/uploads                     POST /internal/uploads/probe
POST /internal/uploads/from-url            POST /internal/versions/:id/cache
POST /internal/versions/prune              POST /internal/sources/poll

# monitoring and notifications
POST /internal/monitor/run                 POST /internal/notify/test

# called by the edge, not the dashboard
ALL  /internal/authz                       (forward_auth, and the one route the
                                            shared-secret hook lets through)

There is no per-device install route. Installing one app on one box is a rollout with one device in it, which is why POST /internal/rollouts is the only way in: the canary, the concurrency caps, the retries and the job history are all properties of a rollout, and a second path to installing would be a second place for them to be forgotten.

On this page