| title | ADR-009: Database — SQLite | ||||
|---|---|---|---|---|---|
| status | accepted | ||||
| date | 2026-04-22 | ||||
| type | adr | ||||
| tags |
|
Status: Accepted Date: 2026-04-22
The gateway persists POSITION packets — the single v1 wire packet type per ADR-013. (Earlier drafts of this ADR also referenced a separate RELAY_INFO packet from ADR-012; that packet type is rolled back. Tags and relays both self-report as POSITION; rows for both land in the same tag_reports table.) The gateway is the only writer — a single Rust task draining a channel from the LoRa RX thread — and the only reader — the kiosk UI rendering markers. Workload:
- Hundreds to a few thousand rows per day under current planning assumptions
- One process, one machine, one disk
- No concurrent writers from separate processes
- No distributed query, no network client (see ADR-008)
- Must run offline, survive power cycles, be backed up by copying one file
SQLite. Single file at /var/lib/lora-sar/sightings.db on the Pi. Accessed from Rust via rusqlite or sqlx-with-SQLite — choice deferred to the persistence crate implementation.
The DB file is named
sightings.dbfor historical/conceptual continuity; the v1 table istag_reportsper ADR-013. A future schema change may rename the file; the table name is authoritative.
- No DB server process. No
postgresql.service, nopg_hba.conf, no port open, nopostgresuser, no vacuum cron. Yocto image shrinks, attack surface shrinks, failure modes shrink. - Backups are trivial.
cp sightings.db sightings.db.bakwhen idle, orsqlite3 sightings.db ".backup '/mnt/usb/backup.db'"for a consistent online copy without stopping the gateway. - One writer, one reader, no contention. WAL mode gives the UI thread snapshot reads while the LoRa thread writes. Standard SQLite idiom.
- Schema lives in a
persistencecrate next toprotocol. Migrations applied idempotently on gateway startup. seq_nrwidth and dedup policy. Wireseq_nrisu32in theprotocolcrate, big-endian (see ARCHITECTURE.md §7). An earlier sketch usedu16; at the planned 300 s heartbeat cadence, au16wraps in ~227 days, and at a 45 s minimum SOS interval in ~34 days. Combined with a permanentUNIQUE INDEX idx_dedup ON tag_reports(node_id, seq_nr)— which was also in an earlier sketch — the gateway would start silently dropping real new reports as "duplicates" after the first wrap. That is a system-killing drift bug that only appears months into deployment. The fix is threefold and all three parts are non-negotiable:- Wire-level
seq_nrisu32. Au32at the worst-case minimum interval still wraps only on geological timescales; at that horizon the appliance has been re-flashed many times. - Dedup is enforced over a recent-window filter (e.g. last 24 h of rows per
node_id), not a permanent table-wide constraint. Implementation: a narrow covering index on(node_id, seq_nr, received_at DESC), and anINSERT ... WHERE NOT EXISTS (SELECT 1 FROM tag_reports WHERE node_id = ? AND seq_nr = ? AND received_at > NOW - 86400). TheUNIQUE INDEXfrom the earlier sketch is removed. - Dedup is keyed on
(node_id, seq_nr)— singletag_reportstable for tags and relays alike, per ADR-013. (The earlierRELAY_INFO-with-(relay_id, seq_nr)formulation from ADR-012 is rolled back.)
- Wire-level
- Future cloud sync not blocked (though not built).
sqlite3 .dump | psqlis a trivial export path.tag_reportshas a monotonic integer PK and areceived_attimestamp — both sufficient for incremental sync to whatever Postgres schema shows up in v2. - Spatial queries. Not needed in v1 — the kiosk does bounding-box filtering on the Pi, and SQLite handles a few thousand lat/lon rows trivially. PostGIS is a v3+ concern if ever.
- PostgreSQL. Rejected. Buys nothing at this scale: no concurrent writers, no network clients, no spatial index needs. Costs a server daemon, RAM, an admin surface, and a second failure mode. Right choice for a cloud backend — which v1 doesn't have.
- Flat CSV or JSONL log. Rejected: no indexed reads, no transactional safety, manual corruption handling.
- Embedded K/V (sled, redb). Rejected: we actually want SQL — range queries on
(node_id, received_at)are the primary read pattern, and SQLite is the most battle-tested embedded SQL store by orders of magnitude. - DuckDB. Rejected for v1: analytical workload fit, not the transactional write path we need.