Infrastructure
Overview
Section titled “Overview”| Layer | What lives there |
|---|---|
| Supabase | PostgreSQL database, auth, realtime, row-level security |
| AWS | Data processing pipeline, Lambda artifact builds, S3 data bucket for geodata |
| Cloudflare | DNS for both domains, and the Worker CDN serving data.burglarysupport.com |
The webapp talks directly to Supabase. Processed geodata (district boundaries, crime stats) is written to S3 by the Lambda pipeline and served via a Cloudflare Worker at data.burglarysupport.com.
Terraform config lives in terraform/. The AWS module is at terraform/modules/aws/.
Data processing pipeline
Section titled “Data processing pipeline”Triggered weekly by EventBridge on Sunday at 04:00 UTC. The same workflow can be triggered manually via the BSN CLI or by starting an execution in the Step Functions console.
Step Function: burglary-data-processing-workflow
Section titled “Step Function: burglary-data-processing-workflow”DataIngestion (geodata-ingestion) └─ ParallelProcessing ├─ DistrictProcessing (geodata-districts) → NeighborCalculation (geodata-neighbors) └─ FetchCrimeData (data-fetcher) └─ CrimeDataEnrichment (geodata-crime-enricher) └─ MetadataCreation (geodata-metadata) └─ ProcessingSucceeded / ProcessingFailedAll tasks retry up to 3 times with exponential backoff on Lambda service errors.
Lambda functions
Section titled “Lambda functions”| Function name | Memory | Timeout | Role |
|---|---|---|---|
burglary-geodata-ingestion | 1 024 MB | 10 min | Download raw postcode data; publish the monolith and the per-outcode shards |
burglary-geodata-districts | 1 024 MB | 10 min | Compute district bounds |
burglary-geodata-neighbors | 512 MB | 5 min | Compute district neighbour relationships |
burglary-data-fetcher | 512 MB | 5 min | Fetch crime data from UK Police API |
burglary-geodata-crime-enricher | 2 048 MB | 15 min | Join crime records to districts (3 years of data) |
burglary-geodata-metadata | 256 MB | 3 min | Write aggregate metadata |
burglary-supabase-keepalive | 128 MB | 30 s | Ping Supabase daily to prevent free-tier suspension |
Published geodata
Section titled “Published geodata”The ingestion function writes the postcode dataset (~1.73M postcodes) in two shapes, for two different consumers:
| Key | Size | Consumer |
|---|---|---|
geodata/postcode_locations.json | ~103 MB | geodata-crime-enricher only — it loads the whole thing in memory to join coordinates onto crimes. Never fetch this from a browser. |
geodata/postcodes/{outcode}.json | ~25 KB each, ~2 900 files | The webapp’s postcode lookup (packages/webapp/src/shared/lib/geocode.ts). Keyed by space-stripped postcode, values are [lat, lng]. |
geodata/postcode_district_data.json | ~744 KB | The map — district centroids and bounds. |
The shards exist because the monolith is not addressable per postcode: resolving one postcode from it means downloading all 103 MB. Splitting by outward code is what makes client-side geocoding possible without a third-party lookup; postcodes.io is only a fallback for postcodes newer than the last pipeline run.
Shards are rewritten whenever the source ZIP’s hash changes, at 25 uploads in parallel, and the run fails loudly if any shard fails rather than leaving a partially-published set. Worth watching on the first run against the function’s 10-minute timeout.
All functions run Node.js 20 on arm64. Processed data is read and written to the S3 bucket bsn-production-data-eu-west-2 (in the dedicated BSN account, 582093722364). Lambda artifacts are pulled from s3://bsn-production-data-eu-west-2/lambda-artifacts/<name>.zip.
The supabase-keepalive function fires on its own EventBridge schedule (daily at 06:00 UTC) and queries a real table (user_profiles) so the request registers as database activity. Pinging the PostgREST root (/rest/v1/) does not work: it is served from the schema cache without touching Postgres, and it rejects the anon key with 401 (only the service_role key may call it). It is unrelated to the data pipeline.
Running the pipeline manually
Section titled “Running the pipeline manually”# Start incremental run (skips months already processed)npm run bsn -- aws stepfunction start
# Force full reprocessingnpm run bsn -- aws stepfunction start --force
# Invoke a single function for debuggingnpm run bsn -- aws lambda crime-enricher --month 2024-03CodeBuild artifact pipeline
Section titled “CodeBuild artifact pipeline”Terraform provisions a CodeBuild project (burglary-support-lambda-builder) that builds and uploads Lambda zips whenever packages/aws-lambdas/** changes on main.
- A push to
maintouchingpackages/aws-lambdas/triggers the GitHub webhook. - CodeBuild clones the repo (GitHub PAT stored in
var.github_oauth_token). esbuildbundles each Lambda from TypeScript directly — notscstep. Workspace symlinks fromnpm installresolve@burglary-support/shared-types.- Zips are uploaded to
s3://burglary-support-data-eu-west-2/lambda-artifacts/. - The next Lambda invocation (or a manual
aws lambda update-function-code) picks up the new zip.
Lambda functions are not automatically updated after a build — CodeBuild only publishes the artifact. Use make lambda-deploy or the AWS console to push the new zip to a function.
Data CDN (Cloudflare Worker → S3)
Section titled “Data CDN (Cloudflare Worker → S3)”A Cloudflare Worker (bsn-data-proxy) serves the S3 data bucket — this
replaced the old CloudFront distribution during the dedicated-account
migration:
- Domain:
data.burglarysupport.com(Cloudflare Universal SSL;data-staging.burglarysupport.comis the pre-cutover verification hostname) - Origin: the S3 REST endpoint, locked to requests carrying the shared secret the Worker sends (
s3-origin-lock.tf) - CORS: GET/HEAD/OPTIONS allowed from any origin, 24-hour max-age
- Cache TTL: 24 hours for success responses, errors barely cached
# Purge the edge cache after deploying new geodatamake data-cache-purgeRunning terraform apply
Section titled “Running terraform apply”cd terraformterraform initterraform planterraform applyRequired variables — pass as TF_VAR_* env vars or in a terraform.tfvars file:
| Variable | Notes |
|---|---|
TF_VAR_cloudflare_api_token | Zone:Read, Zone:Edit, Account:Read, User:Read |
TF_VAR_cloudflare_account_id | |
TF_VAR_github_oauth_token | PAT with repo scope for CodeBuild webhook |
TF_VAR_supabase_anon_key | Used by the keepalive Lambda |
Cloudflare secrets are stored in SOPS at ~/own/go-setup/k8s/server/secrets.yml. Decrypt with sops -d before running Terraform if you need to pass them as env vars.
Other variables (aws_region, s3_bucket_name, environment, etc.) have sensible defaults in terraform/variables.tf and generally do not need overriding.