- 9
- Devices reporting
- 41,000
- Metrics ingested/day
- 180 ms
- Dashboard latency
- 99.7 %
- Uptime
Pulse exists because I got tired of checking four different tabs to know what my hardware was doing. The weather station had its own page, the print farm had OctoPrint, the home hub logged to a file nobody read. I wanted one screen on the wall that answered “is everything okay?” at a glance.
Architecture
Devices POST JSON readings to a single ingest endpoint on a SvelteKit server running on my home server (an old ThinkCentre pulling 11 W idle). Readings land in SQLite via better-sqlite3 — synchronous writes, WAL mode, no ORM. The dashboard page opens one EventSource connection and the server fans out every new reading to connected clients. There is no message broker and no separate API service; the whole thing is one Node process behind Caddy.
Why SSE, not WebSockets
I started with WebSockets out of habit, then realized the data flow is strictly one-directional: server pushes, browser renders. SSE gave me automatic reconnection with Last-Event-ID for free, worked through Caddy with zero config, and deleted about 120 lines of heartbeat and reconnect code. The lesson: pick the transport that matches the traffic shape, not the one with the bigger feature list.
Time-series in SQLite
The naive schema — one row per reading — worked until the raw table hit 2 million rows and the 24-hour chart query took 900 ms. The fix was a rollup table populated on write:
INSERT INTO readings_hourly (device_id, metric, hour, min, max, avg, n)
VALUES (?, ?, strftime('%Y-%m-%dT%H', 'now'), ?, ?, ?, 1)
ON CONFLICT(device_id, metric, hour) DO UPDATE SET
min = MIN(min, excluded.min),
max = MAX(max, excluded.max),
avg = (avg * n + excluded.avg) / (n + 1),
n = n + 1;
Charts longer than six hours read the rollups; only the live sparklines touch raw rows. Query time dropped to under 20 ms, and a nightly job deletes raw rows older than 90 days.
Keeping it up
Uptime is currently 99.7% over five months. The two outages were both mine: a full disk from forgetting WAL checkpointing, and a power cut the UPS I hadn’t bought yet would have absorbed. A systemd watchdog and a disk-space stat on the dashboard itself closed the loop — Pulse now monitors Pulse.
Development timeline
- 2025-12
First ingest
Weather station POSTing readings into a bare SQLite table.
- 2026-01
SSE live view
Dropped the 5-second polling loop for server-sent events.
- 2026-03
Rollups and retention
Hourly aggregation tables cut dashboard queries from 900 ms to under 20 ms.
- 2026-05
Shipped
Running unattended behind Caddy with systemd watchdog restarts.