Secure Bike Parking: Scoring Where to Lock Up

2 min read Updated July 26, 2026

Published by Dan Richardson at https://freepdx.com/deep-dives/secure-bike-parking

Team
Two-person hackathon origin; production build-out led by Dan
My contribution
Production build-out owner
Outcome
A five-hour prototype became a public map and moderated data platform.
Status
Production: Expanding across US & Canada
Constraints
  • The ranking has to degrade safely when crime, imagery, or geocoding sources fail.
  • Human review gates user reports and data imports before they affect public scores.
  • The collaborator contributed to the hackathon origin; Dan owns the later production build-out.

The Problem

Some racks are theft magnets, some are safe. That knowledge lives in riders’ heads, not anywhere you can look it up. What if you could quickly say where you want to go, and then find the most secure place to park your bike when you got there?

What I Built

A map-driven guide that scores each parking spot by theft risk, visibility, and street-level imagery, and lets riders add spots of their own. Two surfaces: a public map and an admin moderation portal. It started as a two-person, five-hour hackathon build and grew into a roughly 500-commit platform.

Tech Stack

Languages
  • TypeScript/TSX
  • SQL
  • JavaScript
Runtime & frameworks
  • React 19
  • Vite
  • React Router
  • Cloudflare Pages Functions
  • Cloudflare Workers
Data & storage
  • Cloudflare D1
  • Cloudflare KV
  • Cloudflare R2
  • Cloudflare Queues
  • browser localStorage
Infra & deploy
  • npm workspaces
  • Wrangler
  • Cloudflare Pages preview branches
  • Worker cron and queue consumer
AI / ML
  • Anthropic Claude Haiku 4.5 for NL parsing
  • explanation generation
  • submitted-photo vision judging
Testing
  • Vitest
  • Playwright
  • Node test runner
  • TypeScript project references
  • OpenAPI codegen drift check
Notable libraries
  • Leaflet
  • leaflet.markercluster
  • jose
  • openapi-typescript
  • exifr
  • heic2any
  • fflate

How It Works

The recommendation pipeline fuses several mismatched sources - Nominatim geocoding, OSM and Overpass spots, PortlandMaps crime data, Google Street View and Mapillary imagery - and scores each one. Claude vision judges the spots; a lighter model parses trip requests in plain language. User submissions run through a human-in-the-loop ingest pipeline. Both front ends generate their types from the same OpenAPI spec, and a codegen drift check fails if those types fall out of sync. Playwright separately covers the public and admin UI paths.

Secure Bike Parking: five fused signals, one API, two apps Five mismatched sources feed the scoring engine: Nominatim geocoding, OSM and Overpass bike-parking spots, PortlandMaps crime data, Google Street View and Mapillary imagery, and rider-submitted photos. apps/api fuses them into one composite score, using Claude Haiku 4.5 to parse trip requests in plain language and Claude vision to judge submitted photos. Below apps/api sits packages/contracts, an OpenAPI 3.1 spec that is the single source of truth for generated types. A codegen drift check fails when those types are stale. A separate Playwright end-to-end gate blocks shipping an admin UI change without a passing test. Both apps/web, the public map, and apps/admin, the moderation portal, consume types generated from that same spec so the two front ends cannot drift apart. apps/admin also runs human-in-the-loop ingest approval, report moderation with a score-impact preview, and an append-only audit log - a rider's submitted photo only affects the score after an operator approves it there. Secure Bike Parking five fused signals, one API, two apps Nominatim geocoding OSM / Overpass bike-parking spots PortlandMaps crime data Street View + Mapillary imagery Rider photos user-submitted apps/api - composite score Claude Haiku 4.5 parses the trip, plain language Claude vision judges rider-submitted photos weighted score: theft risk + visibility + imagery packages/contracts (OpenAPI 3.1) - source for generated types codegen drift check + Playwright UI tests - both gate changes apps/web - public map scored spots on the map trip recommendation card reads the same generated types as admin apps/admin - moderation portal human-in-the-loop ingest approval report moderation, score-impact preview append-only audit log
Five mismatched sources fuse into one score behind apps/api. The OpenAPI spec in packages/contracts generates types for both front ends. A codegen drift check keeps them aligned with the API, while Playwright gates the UI.

What Made It Hard

Fusing messy, mismatched geodata into one score a rider can trust. Building a moderation pipeline a human can actually review at speed. Keeping two independent front ends aligned with the same API through generated types, a codegen drift check, and end-to-end UI tests.

The Throughline

A civic tool for my own city: take scattered, hard-won local knowledge and make it usable in ten seconds by anyone. The shared API, generated types, codegen drift check, and UI tests let it grow without the public site and admin portal drifting apart.

Status

Live at securebikeparking.org, grown from the hackathon demo into a working platform.

Pinch or drag to inspect