Overview
Dopesilicon is a modular cloud IoT platform. Devices publish telemetry over MQTT, a backend ingests those messages into InfluxDB, and a React dashboard visualizes them through a REST API. Every component runs as a container, which makes the whole stack deployable to a single server or to cloud container platforms without changing application code.
The platform treats a device as a set of named measurements. The plumbing between the device and the dashboard is generic: the backend never parses device payloads directly, it routes whole topics into the time-series store. Adding a new measurement is a data-model change, not an architecture change.
ingress (transport)
│
┌─────────────────┼──────────────────┐
▼ ▼ ▼
publisher ──► Mosquitto broker ──► backend (Node.js)
▲ │ │ writes (Points)
│ │ InfluxDB
│ nginx proxy │ reads (Flux)
dashboard (React) ◄──────────────────────┘Components
The stack (docker-compose.yml) runs five services on an isolated bridge network:
| Service | Image / stack | Port(s) | Role |
|---|---|---|---|
mosquitto | Eclipse Mosquitto | 1883, 9001 | MQTT broker (TCP + WebSocket) |
back-end | Node.js 20 + Express | 3000 | MQTT subscriber, REST API, DB access |
influxdb | InfluxDB 2.x | 8086 | Time-series storage |
front-end | React 18 (nginx-served build) | 80 (internal) | Web dashboard |
nginx | nginx:stable-alpine | 80, 1883 | Reverse proxy, MQTT stream |
influxdb and mosquitto persist to named volumes so data survives container restarts; the backend reads its configuration from an .env file mounted through Compose.
Data Model
The data model lives in back-end/constants.js and is shared by every database module:
const measurements = {
temperature: "temperature",
sound: "sound",
heart_rate: "heart",
sleep: "sleep",
walking: "walking",
jogging: "jogging",
steps: "steps",
biking: "biking",
idling: "idling",
oxygen: "oxygen",
};
const devices = { device_1: "device_1" };
const tags = { device: "device" };
const fields = {
degrees: "degrees",
sound: "sound_type",
beats_per_inute: "beats_per_minute",
sleep_stage: "sleep_stage",
steps: "steps",
minutes: "minutes",
percentage: "percent",
};Every stored point is an InfluxDB Point carrying three things: a measurement name that identifies the signal, a device tag that says where it came from, and a field with the actual value. Device as a tag is what allows multiple hardware nodes to share one platform.
Ingestion Path
Devices publish to per-measurement topics defined in back-end/mqtt/channels.js, e.g. measurement/temperature, measurement/heart, measurement/sleep.
The backend connects to the broker through subscriber.js, subscribes to the full topic list on connect, and becomes the recording endpoint:
client.on('message', (topic, message) => {
if (topic === ch_temperature) {
writeTemperature(message);
} else if (topic === ch_heart) {
writeHeartRate(message);
}
// ...
});The write layer (db_write.js) converts each message into an InfluxDB point and tags it with the device, then defers the write and flushes in batches — points are queued and flushed on a schedule, which aggregates many small messages into efficient HTTP writes:
let point = new Point(measurements.temperature)
.tag(tags.device, devices.device_1)
.intField(fields.degrees, degrees);
setTimeout(() => writeClient.writePoint(point), 1000);
setTimeout(() => writeClient.flush(), 5000);Storage, Bootstrapping, and Secrets
InfluxDB is provisioned on first boot by database/db_init.js. It calls the InfluxDB setup API to create the admin user, an organization, and a bucket (with retention), then issues a scoped API token used by the read, write, and delete clients.
Credentials are not hard-coded. The backend reads an InfluxDB API token from AWS Secrets Manager (secrets/aws_secrets.js), using EC2 instance-metadata credentials. On first run the secret does not exist, so the backend provisions InfluxDB, stores the new token in Secrets Manager, and continues — subsequent boots skip provisioning and read the stored token. This makes the deployment self-bootstrapping on a fresh cloud instance.
Read Path and REST API
The HTTP API (router/general.js) is a small Express router. Every endpoint accepts an optional time parameter so the frontend can ask for "everything since boot" (0), "since a date until now" (ISO string), or "the last N days" (Nd with a d suffix):
| Method | Route | Returns |
|---|---|---|
| GET | /api/all | Raw time-series records |
| GET | /api/temperature/:date? | Temperature over the range |
| GET | /api/heart/:date? | Heart-rate samples |
| GET | /api/sleep/:date? | Sleep series + time-per-stage summary |
| GET | /api/steps/:days? | Daily step totals (windowed) |
| GET | /api/sound, /api/action, /api/walking, /api/jogging, /api/biking, /api/idling, /api/oxygen | Corresponding series |
Queries are Flux, built per request:
from(bucket: "fitBucket")
|> range(<start>[, stop: <now>])
|> filter(fn: (r) => r._measurement == "steps" and r.device == "device_1")
|> window(every: 1d)
|> last()Two patterns recur: simple range filters for continuous series (temperature, heart rate, sound) and window() + last() aggregation for counters like steps. The sleep endpoint additionally converts the raw stage stream into a time breakdown (db_read.js → getTimeSummary()), computing minutes and percentages spent in awake/light/rem/deep sleep.
CORS is enforced from an allow-list built from environment variables (FRONTEND_HOST, HOST_URL) so only the deployed dashboard origin can call the API.
Frontend Dashboard
The dashboard is a create-react-app build (front-end/) — React 18, react-bootstrap for layout, and Chart.js for visualizations. It is intentionally thin: there is no client-side data model of its own.
ApiContext.jsfetches every API endpoint concurrently withPromise.allon mount, then folds the responses into a single dashboard state object (sleep stages, activity durations, heart-rate and oxygen series, step totals).- Chart components (
HeartChart,SleepingLine,ActionChart,Bar,SleepSummary,Cards) subscribe to that context and render the data with Chart.js — a line chart overlaying heart rate and SpO₂, a stepped sleep-stage line, a doughnut and progress cards for activity, and a daily step bar chart. - In development the API base URL is
REACT_APP_BACKEND_URL; in production the frontend calls relative/api/*paths that nginx forwards to the backend, so the browser never talks to different hosts directly.
Deployment
Docker Compose (single server)
docker-compose.yml wires all five services together with dependency ordering (frontend waits on backend, backend waits on influxdb and mosquitto) and persistent volumes for the database and broker. Provisioning scripts in the repo automate bringing a fresh server up: deploy_ubuntu.sh installs Docker and Compose on Ubuntu, and deploy.sh targets Amazon Linux.
Reverse proxy
nginx plays two roles:
- HTTP —
/serves the React build from the frontend container,/api/proxies to the backend on:3000, and/mqtt/upgrades to a WebSocket for browser MQTT clients connecting to Mosquitto on:9001. - TCP stream — a
streamblock proxies the outside world to Mosquitto's:1883, so devices on the internet can reach the broker at the server's public address.
AWS
Dockerrun.aws.json describes the same stack for AWS Elastic Beanstalk's multi-container Docker environment, mapping each service to persistent host volumes. Combined with Secrets Manager for the database token, this is the path the platform will use when it goes live at dopesilicon.com.
Design Notes
- The onboarding was built first for a wearable activity tracker, so the current measurement set and dashboard layout are shaped by that first device. The transport, storage, and query layers are not device-specific — the hydroponics controller plugs into the same pipeline by publishing its own measurements.
- Batching writes (queued points, scheduled flushes) trades a little latency for far fewer HTTP calls to InfluxDB, which matters when a device publishes multiple times per second.
- Tagging every point with the device keeps multiple hardware nodes in one bucket and one API surface.
- The backend subscribes to the full topic list at startup; a device's first published message is the only registration the platform needs.
Roadmap
- Dynamic device and measurement registration so nodes self-describe on first connect
- Configurable dashboards instead of the fixed chart layout
- Broker authentication and TLS (MQTTS / WSS) for production device onboarding
- Per-device and per-user access control
- Retention policies per bucket and data lifecycle management
- Integration with the hydroponics controller as the first non-wearable device