Device Architecture
:::info Status
Implemented. Source: docs/device-management-module-architecture.md, docs/device-management-architecture-summary.md.
:::
:::caution Documentation gap in the source
docs/device-management-module-architecture.md still headers itself
"Status: Design / not implemented." That header is stale: the migrations
(fastapi-backend/migrations/versions/20260824_*, 20260827_*), the
device_inventory/devices/device_ownership/device_assignments/
device_audit_log tables, and core/device_registry.py's transition matrix
match this document's §5c almost exactly, including the ticket references
(AZD-50, AZD-51 and others) in the code's own docstrings. Treat this page's
"Implemented" status as verified against the running code, not against that
header — and treat the header itself as a documentation bug to fix at the
source.
:::
Two sold device types, one free client
| Transmitter | Stream Player | Mobile App | |
|---|---|---|---|
| What | Mosque amplifier driver | Member-owned receiver/player unit | Android / iOS client |
| Is it a device? | Yes | Yes | No — lives in app_installs, never devices |
| Commercial | Sold | Sold; a member may buy several | Free to all users |
| Entitled by | Super Admin authorization | Super Admin authorization and owner's approved membership of the bound mosque | Approved mosque membership |
| Inventory-tracked | Required | Required | None |
| Also does | — | Bluetooth speaker, ungated | — |
Why app_installs isn't a row in devices. A device_kind discriminator
would force every consumer of the devices table to branch on it. Keeping
them fully separate gives devices one rule, no exceptions — see
App Installation module.
The separation principle
Five concepts are kept deliberately separate throughout this system. This is the architectural decision that everything else in device management flows from — see each concept's own page for full detail:
| Concept | Page | Never changed by |
|---|---|---|
| Identity | Identity | rebinding, membership, approval |
| Ownership | Ownership | rebinding, membership, approval |
| Authorization | Device Authorization | membership, rebinding, the owner |
| Mosque binding | Mosque Binding | approval, ownership transfer |
| Playback entitlement | Playback Entitlement | — never stored, derived live |
A gift changes ownership without touching binding. Moving house changes binding without touching ownership. A warranty replacement changes the inventory row under a stable owner and binding. The three relationships are never assumed to move together — see Ownership for the full transfer/replacement mechanics.
The client is never the source of truth
What a device or app claims about itself grants nothing — identity claims are lookup keys, not evidence. The backend resolves identity, inventory, ownership, binding, live membership, and lifecycle on every stream request and every trigger:
stream_urlis withheld from an unentitled subject's config, not merely unused.- An MQTT
playcommand is an instruction to try, not a grant — the device still has to fetch a stream token, re-checked against live membership. - Membership is read live on every check, never copied onto the device row.
- Triggers resolve to per-device authorized endpoints, never a shared topic.
Full mechanics: Device Authorization.
Bluetooth is outside the perimeter
A stream player is a speaker the member bought. Bluetooth playback is
never gated — not by lifecycle_status, membership, inventory, or
backend reachability — and never reported in detail (only a generic
PLAYING with source:"bluetooth", so the fleet panel shows the speaker as
busy without exposing what's playing over it).
Azan360 authorization governs only Azan360 audio. Bluetooth remains a completely independent local audio capability.
The one place the two paths meet is audio arbitration — which source
gets the speaker at a given moment (bluetooth.azan_interrupts, default
true; see MQTT Protocol). That's a mixing decision,
never a permission one: a BLOCKED, REVOKED, UNBOUND, or
lease-expired unit has no azaan to arbitrate, and its Bluetooth path behaves
exactly as it would on a fully authorized unit. For support purposes, "my
Azan360 box went silent" and "my Bluetooth speaker stopped working" are
different faults with disjoint causes — only the first is ever this
system's doing.
Denial matrices
| Subject | Stream and trigger require |
|---|---|
| Transmitter | in the registry, ACTIVE, inventory-bound, assigned to the requested mosque |
| Stream player | everything a transmitter needs, plus the owner's membership of the bound mosque must be approved. Bluetooth stays ✅ on every row, including BLOCKED, REVOKED, LOST |
| App install | enrolled, ACTIVE, approved membership of the requested mosque. Enrollment is registration for addressability (push tokens, preferences, audit, the block switch) — not authorization on its own |
Where to go next
- Device Lifecycle — the full state machine
- Device Management module — the registry API and the cutover rollout risk
- Transmitter module and Stream Player module — kind-specific behavior
- MQTT Protocol — the wire contract every device speaks
- Database — entities — the actual tables