Skip to content

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.

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.