Apps
Experimental feature
Everything else on this site is a static mkdocs build: essentially free to host, and with no real end-of-life, since the built pages just sit on GitHub Pages indefinitely. Apps are different — by definition they need to do something at runtime, and that has to run somewhere.
The project is funded for a fixed term, so the guiding rule for anything interactive is: keep whatever costs money to run as cheap, small, and separable as possible, so that as much of it as can survive does, after the funding stops. In practice that means:
- Data lives separately from compute. Datasets (crime data, boundaries, features, hex aggregates, ...) sit in Azure Blob storage rather than being baked into an app or a database. Blob storage is cheap to keep around; an always-on app or database service is not. Whichever tier below is doing the computing, it reads the same data from the same place, so the data's cost and lifecycle aren't tied to whichever compute happens to be fashionable.
- Compute is pushed as far down this list as it will go, reaching for a heavier (and more expensive) tier only when the one above genuinely can't do the job.
Static: compute in the browser
No backend at all — the "app" is static files, served for free exactly like the rest of this site, and any computation happens client-side.
The Crime Capture Explorer works this way. It's built with Observable Framework, ships DuckDB-WASM to run SQL queries in the browser, and renders with deck.gl and MapLibre. The browser pulls parquet files straight out of Blob storage — via a short-lived, read-only SAS token — and does the aggregation itself. There is nothing between the data and the client except a static file server.
Marimo notebooks, exported to HTML+WASM, take the same approach for exploratory analysis rather than a purpose-built app.
The catch is that only data and packages that fit comfortably in a browser can work this way: large national datasets need pre-aggregating before they're published for this tier, and some tooling (DuckDB's spatial extension, geopandas, and similar) doesn't yet run reliably under Pyodide/WASM. See the notebooks page's caveats for the detail — the same constraints apply here.
Lightweight backend: a free tier, trimmed to fit
Some interactivity genuinely needs server-side compute — aggregating larger datasets, or geopandas operations that don't have a browser-friendly story yet. Where that's true, the next cheapest option is a small backend on somebody else's free tier, with the data and scope trimmed down to fit within its limits.
https://safer-streets-test.streamlit.app/ is a demo on Streamlit Community Cloud's free tier. To fit, it's restricted to a single Police Force Area, three years of data, and a subset of spatial units — see the Crime Concentration Explorer write-up for why, and what's missing as a result.
The trade-off is obvious: it's free, but capped by someone else's resource limits, and it only stays around for as long as that platform keeps offering a free tier.
Full backend: for the life of the project
For the full, un-trimmed datasets and production-grade interactivity, there's a proper backend: a FastAPI geospatial data API in front of the Blob storage data, and a Streamlit app in front of that, both containerised and deployed to Azure App Service.
- Crime Concentration Explorer app
- Geospatial crime data API (requires an API key)
This tier is deliberately the exception rather than the default: it's funded for the life of the project, but unlike everything above it, it does not survive on its own once that funding stops. Anything that needs this much compute is a candidate for being pushed down a tier later once the data or the query pattern allows it — the Crime Capture Explorer, for instance, started as a page in this Streamlit app and now also exists as the fully static, client-side version described above.
Which to use
| Static | Lightweight backend | Full backend | |
|---|---|---|---|
| Cost | free | free, but capped | funded, ongoing |
| Compute runs | in the browser | shared free tier | dedicated container |
| Data | pre-aggregated/thinned | trimmed subset | full |
| Outlives the project | yes | yes, as long as the free tier does | no |
Default to static. Reach for a backend only when the data or the computation won't fit in a browser, and prefer the free tier over the funded one unless the trimmed-down data genuinely isn't good enough for the job.