ChaterBox
A control plane for a network of physical Raspberry Pi devices across a tree of companies — schedule audio, push messages, watch live device telemetry.
Physical devices reporting live telemetry to one dashboard.
- Role
- Tech lead + PM — 1 backend, 1 frontend, plus design
- Timeline
- Multi-month contract
The business problem
An Amsterdam entrepreneur wanted to sell a product: a network of small physical devices (Raspberry Pi units, "ChaterBox") installed at business locations that play scheduled audio — announcements, messages, music. The pain it solved: a company with many locations and many devices had no central way to organize them and push the right message to the right places on a schedule. You'd be walking device to device. The product was a control plane for all of it — a tree of companies and subsidiaries, with devices hanging off each, driven from one dashboard.
The constraints
- Hardware in the loop. This wasn't just a web app — real Raspberry Pi devices had to report in and receive messages. That constrains everything: a device can be offline, slow, or on bad Wi-Fi.
- A small team I had to run, not just code in. One backend, one frontend, plus design — I was tech lead and PM: I reviewed and fixed their code, set scope, and paid them.
- A client who kept moving. The business idea — and the Raspberry Pi direction — wasn't settled. That's the constraint that eventually ended the project.
What I built and why
A React/Redux dashboard over a Node + PostgreSQL backend, with Google OAuth for sign-in. The company hierarchy is stored as an adjacency list — companies.parent_id points at the parent company — so a company can nest arbitrarily deep and devices attach to any node. Access is scoped through user_company_access (a user's rights follow the company subtree), and every mutation is written to audit_logs. Internationalization is a polymorphic translations table: any field of any entity can be translated by (entity_type, entity_id, field_name, lang_id), so adding a language never touches the schema — exactly what "support future languages" required. Devices, schedules (with recurrence_pattern), and message targeting (message_assignments with target_type / target_id and per-target delivery status) are all modeled.
The device layer actually worked end to end: a physical Pi reports live telemetry to the server — uptime, CPU load, disk, temperature, Wi-Fi SSID and signal. This was never a mockup.
Technical decisions, with the cost of each
Decision 1 — the tree as an adjacency list in PostgreSQL, not a graph database.
The brief floated a graph DB (Neo4j) for the hierarchy. I kept it in Postgres with a self-referencing parent_id. Reason: the whole system already needed relational integrity, transactions, RBAC, and — critically — PostGIS for geolocation, all in one engine. Adding a second database for the tree alone would have split the source of truth and doubled the ops surface for a two-person team.
The cost: reading a full subtree needs a recursive CTE rather than a native graph traversal, and very deep trees would eventually make those queries something to watch. At this scale, a fair trade for keeping one database.
Decision 2 — two-tier device status: live poll versus cached state.
Two endpoints, on purpose. One reaches out to the actual Pi and returns full live telemetry — authoritative, but slow and dependent on the device being reachable. The other returns the server's cached online/offline — instant and cheap. The dashboard uses the cheap one to render lists; the live poll is on-demand, when you actually open a device.
The cost: the cached status can be briefly stale — a device that just dropped still shows online until the next heartbeat. The alternative, live-polling every device on every page load, would fall over the moment a company has a hundred devices. I chose staleness over a system that doesn't scale.
The result — honestly
- No metrics — the product never reached real users. The client pivoted before launch. I won't dress that up.
- What shipped: the backend and data model, Google-auth login, the main dashboard with the full interaction surface (selection, filter controls, message composition), role scaffolding, and — the part I'm proudest of — real devices reporting live telemetry to the server.
- Where it stopped: the device tree and geographic filtering. The honest reason is boring and correct — all the devices sat in one physical location, so there was no geographic spread to filter, and the admin/superadmin panel to expose it all wasn't built yet. No point finishing a feature with no data to justify it.
- What I take from it: I designed a hardware-backed distributed system and led a team to build most of it. The tree and status decisions are ones I'd defend in any review.