Skip to content

Infrastructure

LayerWhat lives there
SupabasePostgreSQL database, auth, realtime, row-level security
AWSData processing pipeline, Lambda artifact builds, S3 data bucket for geodata
CloudflareDNS 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/.


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 / ProcessingFailed

All tasks retry up to 3 times with exponential backoff on Lambda service errors.

Function nameMemoryTimeoutRole
burglary-geodata-ingestion1 024 MB10 minDownload and store raw postcode/geodata
burglary-geodata-districts1 024 MB10 minCompute district bounds
burglary-geodata-neighbors512 MB5 minCompute district neighbour relationships
burglary-data-fetcher512 MB5 minFetch crime data from UK Police API
burglary-geodata-crime-enricher2 048 MB15 minJoin crime records to districts (3 years of data)
burglary-geodata-metadata256 MB3 minWrite aggregate metadata
burglary-supabase-keepalive128 MB30 sPing 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.

Terminal window
# Start incremental run (skips months already processed)
npm run bsn -- aws stepfunction start
# Force full reprocessing
npm run bsn -- aws stepfunction start --force
# Invoke a single function for debugging
npm run bsn -- aws lambda crime-enricher --month 2024-03

Terraform provisions a CodeBuild project (burglary-support-lambda-builder) that builds and uploads Lambda zips whenever packages/aws-lambdas/** changes on main.

  1. A push to main touching packages/aws-lambdas/ triggers the GitHub webhook.
  2. CodeBuild clones the repo (GitHub PAT stored in var.github_oauth_token).
  3. esbuild bundles each Lambda from TypeScript directly — no tsc step. Workspace symlinks from npm install resolve @burglary-support/shared-types.
  4. Zips are uploaded to s3://burglary-support-data-eu-west-2/lambda-artifacts/.
  5. 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.


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.com is 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
Terminal window
# Purge the edge cache after deploying new geodata
make data-cache-purge

Terminal window
cd terraform
terraform init
terraform plan
terraform apply

Required variables — pass as TF_VAR_* env vars or in a terraform.tfvars file:

VariableNotes
TF_VAR_cloudflare_api_tokenZone:Read, Zone:Edit, Account:Read, User:Read
TF_VAR_cloudflare_account_id
TF_VAR_github_oauth_tokenPAT with repo scope for CodeBuild webhook
TF_VAR_supabase_anon_keyUsed 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.