How it works

All months
Refreshing
Behind the dashboard

How it works

Metro is a read-only dashboard for Metropolitan Police stop and search statistics. Here's how the data flows from the Police API to your screen, and the design decisions behind the way it's cached.

01 — Data flow
01
Police APIdata.police.uk — the Met's full stop-and-search history
02
Weekly workerapps/worker, a one-shot container run via host cron
03
SQLite cacheWAL mode, one file, no server, no exposed port
04
Next.js clientread-only, via the @metro/core repository layer
02 — Cache stats
Months of history cached
37
Precomputed aggregate rows
476
Database servers running
0
Ports the data layer exposes
0
03 — How it's built

The source

All data comes from the public data.police.uk API, specifically the Met's stop-and-search records. Metro fetches the full available history, not a rolling window, so older months stay comparable to recent ones.

Precompute, then read fast

A weekly worker fetches every month, then precomputes one aggregate row per month × age range × type combination and stores only those in the SQLite cache. The dashboard reads those precomputed aggregates, so filtering is a fast indexed lookup, not a scan of raw records. The slow path (ingestion) is fully separated from the fast path (reads).

Read-only

This app never writes to the cache. It only reads and combines the precomputed rows for the filters you choose — month, age range, and search type. The URL stays in sync with your filters, so any view is shareable.

04 — Why SQLite, not a database server
01
Problem

The first version of this dashboard ran a self-hosted MongoDB — an always-on server process with auth to manage, for a dataset that's really just a few hundred precomputed rows, rewritten wholesale once a week by a single writer and read by one app.

02
Solution

Replace it with SQLite (via Drizzle for typed queries): a single file in a Docker volume, in WAL mode so the rare weekly write never blocks concurrent reads. No server process to run, no port to expose, no auth to manage.

03
Result

No DB daemon, near-zero idle memory, and a stronger security posture than v1 — nothing listens on the network for the data layer at all. Reads are pre-computed lookups, not scans of raw records.