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 and store raw postcode/geodata |
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 |
All functions run Node.js 20 on arm64. Processed data is read and written to the S3 bucket burglary-support-data-eu-west-2. Lambda artifacts are pulled from s3://burglary-support-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.