BCycle Map runs on three Cloudflare Workers that each own a distinct slice of the data pipeline: a scheduled poller that fetches GBFS feeds, an HTTP Worker that serves everyDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/samgutentag/bcycle-map/llms.txt
Use this file to discover all available pages before exploring further.
/api/* endpoint the frontend calls, and a daily smoke Worker that runs a live end-to-end poll against every enabled system and files a GitHub Issue on any failure. This page covers manual deployment with Wrangler, the automated CI flow, log tailing, and post-deploy verification.
The three Workers at a glance
| Worker | Config file | Entry point | Trigger |
|---|---|---|---|
bcycle-map-poller | wrangler.toml | src/workers/poller.ts | Cron every 5 min |
bcycle-map-read-api | wrangler.read-api.toml | src/workers/read-api.ts | HTTP fetch |
bcycle-map-smoke | wrangler.smoke.toml | src/workers/smoke.ts | Cron daily at 09:00 UTC |
bcycle-map-poller
The poller is the heartbeat of the system. Every five minutes its scheduled handler callspollOnce() for each enabled entry in systems.json, fetches the GBFS discovery feed and the three required sub-feeds (station_information, station_status, system_information), normalizes them into an internal KVValue shape, and writes two KV keys per system: a system:<id>:latest key for the read API to serve, and a system:<id>:buffer:<yyyy-mm-dd-hh> key that accumulates the hour’s snapshots for compaction into R2 parquet.
At 5-minute cadence the poller issues 288 invocations per day per system — well under the Workers Free tier cap of 1,000 cron invocations per day.
Each active system costs approximately 576 KV puts per day (2 puts × 288 ticks). The Workers Free tier allows 1,000 KV writes per day, so two active systems already exceeds the cap. Disable a system in
systems.json before adding a second one, or upgrade to Workers Paid.bcycle-map-read-api
The read API is a plain HTTP Worker — no cron, no bindings beyond KV and R2. It routes incoming requests by URL pattern to one of several handlers:| Endpoint pattern | What it returns |
|---|---|
GET /api/systems/:id/current | Latest KV snapshot for a system |
GET /api/systems | Cross-system index from R2 (systems-index.json) |
GET /api/systems/:id/activity | Rolling activity log from R2 |
GET /api/systems/:id/trips | Trips derived from R2 parquet over an arbitrary window |
GET /api/systems/:id/snapshots | Down-sampled station-level bike counts from R2 parquet |
GET /api/systems/:id/partitions | R2 parquet partition keys for a time range |
GET /api/systems/:id/stations/:sid/recent | Pre-computed typical hourly availability profile |
POST /api/beacon | Analytics pageview / event ingest |
GET /api/insights | Aggregated analytics events |
GET /api/geocode | Address → lat/lng proxy (uses server-side Google Maps key) |
bcycle-map-smoke
The smoke Worker runs daily at 09:00 UTC. It callspollOnce() — the same function the poller uses — against every enabled system, but without writing to KV or R2. If pollOnce() throws for any system (fetch error, missing sub-feed, or normalization failure), fileIssueIfNoneOpen() opens a GitHub Issue labelled smoke-failure on the configured repo. A second failure on the same day appends a comment rather than opening a duplicate issue.
Deploying manually with Wrangler
Run these three commands from the repo root afternpm install. Each command reads its respective TOML file and uploads a fresh Worker bundle:
*.workers.dev URL. Save the read-API URL — the frontend needs it in VITE_API_BASE.
Automated deployment via GitHub Actions
Thedeploy-workers.yml workflow handles CI deployment on every push to main. It avoids redeploying all three Workers when only one changed.
How change detection works
Achanges job runs first and outputs three boolean flags — poller, read_api, smoke — based on a diff of the files touched by the push:
- Changes to
src/shared/**,package.json,package-lock.json, or the workflow file itself mark all three Workers for redeploy (shared code touches every bundle). - Changes to
src/workers/poller.tsorwrangler.tomlmark only the poller. - Changes to
src/workers/read-api.tsorwrangler.read-api.tomlmark only the read API. - Changes to
src/workers/smoke.tsorwrangler.smoke.tomlmark only the smoke Worker.
if: needs.changes.outputs.poller == 'true', and so on.
Manual dispatch with target selection
The workflow also acceptsworkflow_dispatch with a target input so you can push a single Worker without a code change:
| Secret | Where to find it |
|---|---|
CF_ACCOUNT_ID | npx wrangler whoami |
CLOUDFLARE_API_TOKEN | Cloudflare → My Profile → API Tokens → Workers deploy template |
Tailing Worker logs
Stream live log output from any Worker usingwrangler tail:
console.log, console.warn, or console.error calls from the handler. KV put failures (e.g., daily cap exhausted) surface here before they affect the frontend.
Verifying a successful deployment
After the first 5-minute cron tick fires, the latest KV snapshot should be populated. Verify with a direct API call:stations array of ~85 entries. A 404 not found means the cron hasn’t fired yet — wait up to five minutes and retry.
The Workers Free tier cron scheduler is eventually consistent. The first tick after a fresh deploy may fire up to a full cron interval late. If the endpoint still returns
404 after ten minutes, check wrangler tail bcycle-map-poller for errors.