Building Real-Time IoT Dashboards with LoRaWAN and FastAPI
GridOwl started from a real operational problem: a client needed to monitor dozens of cold-storage sensors across a facility with no reliable WiFi coverage. Cellular was too expensive per device. Ethernet wasn't feasible. The answer was LoRaWAN — and building it taught me more about hardware-software interfaces than any previous project.
What LoRaWAN actually is
LoRaWAN (Long Range Wide Area Network) is a protocol for low-power, long-range wireless communication. A single gateway can cover several kilometres. Sensor devices run on batteries for years. The trade-off: bandwidth is tiny. You're transmitting sensor readings — temperature, humidity, door open/close events — not video or audio.
The devices talk to a LoRaWAN gateway. The gateway forwards uplinks (device → network) to a network server. We use ChirpStack as the open-source network server. ChirpStack normalises the data and fires webhooks or MQTT messages for each device event.
The architecture: five layers
Sensor devices → LoRaWAN gateway → ChirpStack (MQTT)
→ FastAPI (ingest + business logic)
→ PostgreSQL (time-series storage)
→ React dashboard (WebSocket live updates)
Ingest layer: FastAPI subscribes to ChirpStack's MQTT broker. Each uplink is decoded (device payload → human-readable values), validated, and written to Postgres. We store raw bytes as a fallback alongside decoded values.
Business logic: Alert policies live in the database, evaluated on every ingest. If a sensor exceeds threshold, a background task fires the notification (email, SMS, webhook) without blocking the ingest path.
Storage: Postgres with a readings table partitioned by device and time. We use TimescaleDB's hypertable extension for automatic partition management and fast range queries. A week of readings for 50 sensors fits in a few hundred megabytes.
WebSocket push: FastAPI manages WebSocket connections per user session. On new readings that match a user's subscribed devices, we push immediately. No polling. No 30-second delay.
The hardest part: payload decoding
LoRaWAN payloads are byte arrays. The device manufacturer defines how bytes map to sensor readings — often with bit-packing to save transmission cost. A 4-byte payload might encode temperature as a 16-bit signed integer in tenths of a degree, humidity as an 8-bit unsigned, and a status flag as a single bit.
We built a decoder registry: each device model has a Python function that takes bytes → dict. New device models get a new decoder, nothing else changes. The registry is stored in Postgres alongside device metadata, so operators can assign decoders without a code deploy.
Multi-tenancy with Row Level Security
GridOwl is multi-tenant from the ground up. A single platform instance serves multiple client organisations. Their data must be strictly separated.
We use Postgres RLS policies on the devices and readings tables. Every API request sets a session variable (SET app.current_org = ?) in the connection, and every RLS policy checks that variable. Even if a bug in application code omitted a WHERE org_id = ? clause, Postgres would refuse to return rows from another organisation.
Combined with integration tests that deliberately try cross-tenant queries, this gives strong confidence the isolation holds.
What I'd do differently
Time-series database: Postgres with TimescaleDB works, but for large deployments (thousands of sensors, years of history) I'd evaluate InfluxDB or QuestDB. The query patterns for time-series data are specific enough that a purpose-built engine pays off.
Device simulator: We built a simulator late in the project for integration testing. It should have been day one. Waiting for physical hardware to test an ingest pipeline is painful; a simulator that fires fake MQTT messages with realistic payloads makes the development loop much faster.
Offline buffer: LoRaWAN gateways occasionally lose connectivity. When they reconnect, they replay buffered uplinks out of order. Our ingest layer handles duplicate readings and out-of-order timestamps, but it was retrofitted. Design for it from the start.
What it looks like running
GridOwl's dashboard shows live readings updating every few seconds for connected sensors, historical charts with configurable time windows, and alert history. The React frontend maintains a single WebSocket connection and routes updates to the right chart component.
Operators can configure alert thresholds, notification channels, and data retention policies without touching code. The system has been running in production since early 2024 with a handful of client deployments.
If you're building something similar — facility monitoring, cold chain tracking, environmental sensing — feel free to reach out. The architecture scales from 5 sensors to 5,000 without fundamental changes.
RGCS
Vancouver-based software & AI studio