Should a Georgia Tech student trust the bus right now — or call the Uber?
MARTA's app tells you when a bus is scheduled. It doesn't tell you that Route 20 is running 31 minutes late today, or that half the scheduled buses never showed up on GPS ("ghost buses"). Transit Ledger ATL polls MARTA's live GTFS-RT feeds every minute into TimescaleDB, computes per-route reliability in real time, and gives riders a straight verdict: take the bus / take an Uber / coin flip — plus an AI campus briefing regenerated by a durable Render Workflow.
Built solo in 24h at Hack RenderATL by a GT student who has been ghosted by the 46 one too many times.
MARTA GTFS-RT (.pb, every 60s)
│ Render Cron Job
▼
Tiger Cloud (TimescaleDB) Render Workflows
hypertables: vehicle_positions, campusBriefing task
trip_delays (226k rows/10min) ├─ gatherStats → Tiger caggs
continuous aggregate: ├─ writeBriefing → Gemini (retried)
route_reliability_15m └─ storeBriefing → briefings table
columnar compression after 2h
│
▼
Render Web Service (Express + vanilla JS)
· Bus-vs-Uber verdict engine (/api/commute)
· Gemini function-calling chat over live SQL (/api/chat)
· Live fleet map colored by route on-time %
· Campus briefing panel (reads workflow output)
- Two hypertables ingesting ~2,900 GPS points + ~226,000 delay samples every 10 minutes.
- A continuous aggregate (
route_reliability_15m) keeps 15-min on-time % per route precomputed — the dashboard chart and every verdict read from it lag-free, no GROUP BY at request time. - Columnar compression on
vehicle_positionsafter 2 hours keeps the free instance small. - Ghost-bus detection joins live GPS against the static GTFS timetable in SQL.
workflows/index.tsdefines a durable task graph with@renderinc/sdk/workflows:campusBriefingfans intogatherStats → writeBriefing → storeBriefingsubtasks, with per-task retry/backoff (Gemini free tier is flaky — retries are load-bearing).- The web service triggers runs via
render.workflows.startTask()(POST /api/briefing/run). - Plus the whole app deploys from
render.yaml: web service + every-minute cron poller.
- Function-calling chat: Gemini gets 5 tools (route reliability, ghost buses, headway gaps, worst routes, commute verdict) and answers questions like "can I make it to Georgia Tech by 9:30?" with real SQL-backed numbers.
- Workflow briefing: Gemini turns live Tiger stats into a 120-word campus commute briefing with concrete advice.
cp .env.example .env # DATABASE_URL (Tiger) + GEMINI_API_KEY
npm install
npm run migrate
npm run poll:loop # terminal 1 — poller every 30s
npm run web # terminal 2 — http://localhost:3000Without RENDER_API_KEY/RENDER_WORKFLOW_SLUG, the briefing button falls back to inline generation — same pipeline, no durability.
- Push to GitHub → Render New > Blueprint → this repo (web + cron from
render.yaml). - New > Workflow → this repo, start command
npm run workflows(Blueprints don't support workflow services yet). - Set env vars:
DATABASE_URL(all three),GEMINI_API_KEY(web + workflow),RENDER_API_KEY+RENDER_WORKFLOW_SLUG(web).
| Var | Notes |
|---|---|
DATABASE_URL |
Tiger Cloud connection string |
GEMINI_API_KEY |
Google AI Studio |
GEMINI_MODEL |
optional, default gemini-3.1-flash-lite |
RENDER_API_KEY |
triggers workflow runs from the web service |
RENDER_WORKFLOW_SLUG |
slug of the dashboard-created workflow service |
- On-time = |delay| ≤ 5 min vs static GTFS schedule (MARTA RT sends absolute times, not delays — we compute them).
- Ghost bus = a trip on today's timetable that started ≥8 min ago but has never appeared on GPS.