Pi IoT Academy
Phase 4 · Save Your Data

Phase 04

Save your data.

In Phase 3 your sensor data was magic — it appeared on the screen and disappeared the moment the API restarted. Now you’ll teach the Pi to remember everything in a real database, and prove that nothing is lost when you pull the plug.

~45–60 min Difficulty: 2 / 3 SQL is fun, actually Software lesson

What “durable” means

Read this first.

Until now, the API kept the latest reading per sensor in memory — in a Python dictionary. Memory is fast and easy, but the moment the program restarts (or the Pi loses power), every value is gone. That’s called volatile storage.

For an IoT lab to be useful, we want durable storage: data survives restarts. We’ll use SQLite, which is a database that lives in a single file on disk. No separate server, no extra service to install — just a file the API reads and writes.

Why SQLite, not Postgres?
Postgres needs its own server, network port, and config. For one Pi with classroom-sized data, SQLite is plenty — and the platform is structured so we can swap it for Postgres later without rewriting business code.
What’s a “repository”?
A small Python class with methods like readings.append(...) that hides the SQL. The rest of the app never writes raw SQL — that’s the seam that makes the storage swappable.
What’s a “migration”?
A script that creates or changes database tables. We use Alembic; the API runs upgrade head on startup, so the schema is always current.

By the end of this phase, the API will:

  • Save every sensor reading to data/sqlite/platform.db.
  • Auto-register devices and sensors on first sight (you don’t configure anything).
  • Hydrate the in-memory cache from the database on startup so the dashboard isn’t empty after a restart.
  • Run a nightly clean-up at 03:00 UTC that deletes readings older than 30 days.

Pull in the new code

~5 minutes · on the Pi

Phase 4 adds new files to the platform repo (the database layer, the retention job). On the Pi:

terminal · on the Pi
cd /opt/iot-platform
git pull
docker compose build api
docker compose up -d

docker compose build api rebuilds the API image with the new Python dependencies (SQLAlchemy, Alembic, APScheduler). up -d restarts only what changed.

Watch the API logs:

terminal · on the Pi
docker compose logs -f api

You should see lines like:

expected
INFO  alembic.runtime.migration  Running upgrade  -> 0001, initial schema
INFO  iot.api  cache hydrated: 0 sensor(s)
INFO  iot.api  retention scheduled at 03:00 UTC, 30-day window
INFO  iot.mqtt  MQTT connected to mosquitto:1883

The first line means Alembic created the database tables. Ctrl+C stops following the logs (the API keeps running).

Checkpoint

Find your data on disk

~2 minutes

The database lives in a single file:

terminal · on the Pi
ls -lh data/sqlite/

You should see platform.db and possibly platform.db-wal / platform.db-shm next to it. Those are SQLite’s “write-ahead log” helper files — they let writers and readers work at the same time without blocking each other.

Backups are easy

Because the entire database is one file, backing it up is as easy as cp platform.db platform-backup.db. To reset the lab, you can rm platform.db and restart the API — Alembic will recreate the tables on boot.

Checkpoint

Inspect with sqlite3

~10 minutes

SQLite has its own command-line tool. Install it if you don’t have it, then open the database:

terminal · on the Pi
sudo apt install -y sqlite3
sqlite3 data/sqlite/platform.db

You’re now in the SQLite shell. Try these:

sqlite> prompt
.tables
.schema sensor_readings
SELECT COUNT(*) FROM sensor_readings;
.exit

Expected:

  • .tables lists the seven tables: devices, sensors, sensor_readings, system_logs, modules, ai_prompts, ai_responses. (The last three are reserved for later phases.)
  • .schema sensor_readings prints the CREATE TABLE statement — this is what Alembic ran for you.
  • SELECT COUNT(*) returns 0 if no readings have been published yet, or a number if Phase 3 was running before you restarted.
SQLite shortcuts to know
  • .tables — list tables.
  • .schema [name] — show the create statement.
  • .headers on + .mode column — pretty-print SELECTs.
  • .exit — quit.
Checkpoint

Pump in fresh readings

~5 minutes

Start the publisher from your laptop again (same command as Phase 3):

terminal · on your laptop
python3 scripts/publish-test-sensor.py \
  --host pi-iot-XX.local \
  --password "<your MQTT password>"

Let it run for at least 30 seconds. You should see the dashboard light up as before. Now, on the Pi, peek at the database again:

terminal · on the Pi
sqlite3 data/sqlite/platform.db <<'SQL'
.headers on
.mode column
SELECT COUNT(*) AS readings FROM sensor_readings;
SELECT id, device_id, metric, unit FROM sensors;
SELECT * FROM sensor_readings ORDER BY id DESC LIMIT 5;
SQL

You should see:

  • A growing readings count — about 2 readings per second (one for temperature, one for humidity).
  • Two rows in sensors: one for temperature, one for humidity, both for microbit-test-01.
  • The five most recent readings with timestamps.
No setup, no migrations to write

You didn’t register the sensor anywhere — the API noticed it on first sight and wrote a row to the sensors table. This is called idempotent upsert: insert if new, do nothing if it already exists.

Checkpoint

Browse the time series via the API

~5 minutes

You don’t need sqlite3 to read the database. The API exposes the same data as JSON. From any machine on the LAN:

terminal · anywhere
curl -s http://pi-iot-XX.local/api/sensors | python3 -m json.tool

This lists every sensor with its latest value. To get a small time series:

terminal · anywhere
curl -s "http://pi-iot-XX.local/api/sensors/microbit-test-01.temperature/readings?limit=10" \
  | python3 -m json.tool

You get the last ten readings, newest first. limit is capped at 1000 to stop a runaway query from filling memory.

Pipe through jq for nicer formatting

If you have jq installed (sudo apt install jq on Linux, brew install jq on Mac), it does syntax highlighting and is much faster than python3 -m json.tool.

Checkpoint

Prove durability

~5 minutes · the moment of truth

The point of all this is that the data survives. Let’s force a restart and see what happens.

  1. Stop the publisher on your laptop (Ctrl+C).
  2. On the Pi, restart just the API:
    terminal · on the Pi
    docker compose restart api
  3. Watch the dashboard. Within a few seconds, the connection pill goes back to connected, and your two cards reappear — with their last values from before the restart, and a STALE badge.

That’s the database working. The API booted up, ran SELECT against the sensors and sensor_readings tables, populated the in-memory cache, and the dashboard saw the snapshot the moment it reconnected.

For an even stronger demo, reboot the whole Pi:

terminal · on the Pi
sudo reboot

Wait for it to come back up (~1 minute). SSH back in, check docker compose ps — everything should be Up. Open the dashboard — the cards are still there.

If the cards don’t come back

Three places to look. docker compose logs api — did the API hydrate? sqlite3 data/sqlite/platform.db "SELECT COUNT(*) FROM sensor_readings;" — is the data actually there? Browser dev tools — is the WebSocket connecting?

Checkpoint

Meet the retention job

~5 minutes

Storage isn’t free. If the publisher runs for a year at 1 reading per second, that’s 31 million rows for one sensor. Most of that history isn’t interesting after a month.

The API has a built-in scheduler. Every day at 03:00 UTC, it deletes sensor_readings rows older than 30 days and writes an audit row to system_logs. The retention window is configurable via the READINGS_RETENTION_DAYS environment variable.

You can browse the audit log via the API:

terminal · anywhere
curl -s "http://pi-iot-XX.local/api/system/logs?limit=10" | python3 -m json.tool

You should see a boot event from when the API last started. After 03:00 tomorrow, you’ll see a reading_retention.run event with how many rows it deleted.

Why structured logs

Lines of text are fine for humans. system_logs rows are structured — the detail_json column has machine-readable fields. The dashboard can read them, alerts can trigger off them, and you can ask questions like “how many readings did retention delete this month?” with a single SQL query.

Checkpoint

Final verification

5 minutes

Tick everything below before declaring Phase 4 done.

Phase 4 sign-off

Phase 4 complete?

You’ve got a real backend. Sensors register themselves, readings are durable, the time series is browsable, and the platform cleans up after itself. Next phase: build a touchscreen dashboard worthy of all this data.

Phase 5: Design the dashboard  →

(The button unlocks when every box above is ticked.)