Skip to main content

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

TransmitterStream PlayerMobile App
WhatMosque amplifier driverMember-owned receiver/player unitAndroid / iOS client
Is it a device?YesYesNo — lives in app_installs, never devices
CommercialSoldSold; a member may buy severalFree to all users
Entitled bySuper Admin authorizationSuper Admin authorization and owner's approved membership of the bound mosqueApproved mosque membership
Inventory-trackedRequiredRequiredNone
Also doesBluetooth 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:

ConceptPageNever changed by
IdentityIdentityrebinding, membership, approval
OwnershipOwnershiprebinding, membership, approval
AuthorizationDevice Authorizationmembership, rebinding, the owner
Mosque bindingMosque Bindingapproval, ownership transfer
Playback entitlementPlayback 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_url is withheld from an unentitled subject's config, not merely unused.
  • An MQTT play command 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

SubjectStream and trigger require
Transmitterin the registry, ACTIVE, inventory-bound, assigned to the requested mosque
Stream playereverything 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 installenrolled, 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