A multi-service demo that simulates industrial-plant sensor readings — generated, stored, streamed live, and charted.
A Python (Flask) backend generates sensor data via a standalone worker, stores it in PostgreSQL, and exposes it through a versioned REST API; an Angular frontend shows a live table and embeds Grafana for charting.
🌐 Live demo: https://sensor-app-frontend.onrender.com — the Live Table tab streams new readings in real time, and the Charts tab embeds the Grafana dashboard. It runs on Render's free tier, so the first request after a while may cold-start for ~30–60s.
- Live table — Angular SPA (standalone components) with explicit loading, empty, and error states; new readings arrive over Server-Sent Events
- Charts — the embedded Grafana "Sensor Readings" dashboard with a metric selector (temperature / humidity / vibration) and a moving-average trend, provisioned as code.
- Versioned REST API —
/api/v1/sensorswith a bounded?limit=, ISO-8601 UTC timestamps, and RFC 9457 JSON errors; plus/healthand/ready. - Sensor Simulator — the simulator runs as its own worker process, not a thread inside the web app.
- Deployable — a free-tier Render blueprint runs the whole stack (see Deploy).
Prerequisites:
Clone the repository and start the full stack:
git clone https://github.com/braboj/demo-sensor-app
cd demo-sensor-app
docker compose upThis starts the frontend, backend, a PostgreSQL database, a one-shot migration step, the data-generator worker, and Grafana:
Grafana ships with a PostgreSQL datasource and the "Sensor Readings" dashboard
provisioned as code (deploy/grafana/provisioning/). The admin login is set via
the GRAFANA_ADMIN_USER / GRAFANA_ADMIN_PASSWORD environment variables (see
docker-compose.yml) — set your own before exposing Grafana beyond local use.
Fetch the most recent readings (the last 100 by default):
curl http://localhost:5000/api/v1/sensors[
{
"id": 1940,
"timestamp": "2026-07-01T06:27:14.147279+00:00",
"temperature": 4.4,
"humidity": 93.14,
"vibration": 0
},
{
"id": 1939,
"timestamp": "2026-07-01T06:27:04.139090+00:00",
"temperature": 11.32,
"humidity": 92.68,
"vibration": 1
}
]Vibration is a coded value: 0 = none, 1 = detected.
Bound the result with ?limit= (an integer 1–100; a non-integer or
out-of-range value returns 400, never a silent fallback):
curl "http://localhost:5000/api/v1/sensors?limit=1"Subscribe to the live stream (Server-Sent Events) — new readings arrive as they are recorded (see ADR-0005):
curl -N http://localhost:5000/api/v1/sensors/streamOperational endpoints: GET /health (liveness — 200 if the process is up) and
GET /ready (readiness — 200 if the database is reachable, else 503).
| Path | Purpose |
|---|---|
backend/ |
Flask REST API + data-generator worker |
backend/src/sensor_api/ |
App factory, config, blueprints (sensors, health), worker |
backend/migrations/ |
Alembic migrations |
backend/tests/ |
pytest suite |
frontend/ |
Angular SPA (standalone components) |
frontend/src/app/home/ |
Live readings table + SSE |
frontend/src/app/charts/ |
Embedded Grafana dashboard |
frontend/src/environments/ |
API / Grafana URLs (dev + prod) |
deploy/grafana/ |
Grafana image + datasource/dashboard provisioned as code |
docs/ |
ONBOARDING, PLAYBOOK, DEPLOY, decisions (ADRs), history |
docs/arc42/ |
Architecture documentation (arc42) |
docker-compose.yml |
Full local stack (backend, frontend, db, grafana, worker) |
render.yaml |
Render free-tier blueprint |
docker compose up (above) runs the whole stack. To work on a service on its
own — PostgreSQL is the only external dependency (docker compose up db starts
one):
Backend (Python 3.12, from backend/):
pip install -r requirements.txt
pytest && mypy src --strict # tests + strict type check
flask --app sensor_api run # dev server (needs a reachable PostgreSQL)
python -m sensor_api.worker # run the data generatorFrontend (Node 22, from frontend/):
npm ci
npm start # ng serve on :4200
npm test && npx eslint . # unit tests + lintThe backend reads its configuration from environment variables — copy the
committed backend/.env.example to backend/.env and adjust. See
Onboarding for full setup and Playbook
for day-to-day commands.
Services are configured through environment variables (documented in
backend/.env.example and docker-compose.yml); the most
important:
| Variable | Service | Default | Description |
|---|---|---|---|
DATABASE_URL |
backend, grafana | — | PostgreSQL connection string. |
SECRET_KEY |
backend | — | Flask secret key (sensitive — set your own). |
CORS_ORIGINS |
backend | — | Allowed frontend origin (never *). |
APP_CONFIG |
backend | production |
Config profile (development / production). |
SAMPLE_INTERVAL_SECONDS |
backend, worker | 10 |
Generator sample interval. |
LOG_LEVEL |
backend, worker | INFO |
Log level for the structured JSON logs (never DEBUG in prod). |
RUN_INPROCESS_GENERATOR |
backend | false |
Run the generator in-process (free-tier only). |
WEB_CONCURRENCY |
backend | 1 |
gunicorn worker count. |
API_URL |
frontend (build) | same-origin /api/v1/sensors |
Absolute backend URL baked into the bundle. |
GRAFANA_URL |
frontend (build) | empty | Grafana dashboard URL for the Charts view. |
GRAFANA_ADMIN_USER / GRAFANA_ADMIN_PASSWORD |
grafana | — | Local admin login (sensitive). |
See Deploy for the full deployment variable set.
The repo ships a render.yaml blueprint that runs the whole
stack on Render's free tier — managed Postgres, the Dockerized backend, the
static frontend, and Grafana. The live demo above runs from this blueprint. See
Deploy for the walkthrough and the free-tier caveats.
- To understand the architecture, see the Architecture docs (arc42)
- To set up a dev environment, see Onboarding
- For day-to-day commands, see the Playbook
- To deploy on Render (free tier), see Deploy
- To learn more about the project, see the Assignment
- To read about the solution, see the Solution
- To contribute, see Contributing
- To leave feedback, visit Discussions
Licensed under the MIT License.