Inspiration
It's 11pm. An outreach worker on a sidewalk is phoning shelters one by one for someone who needs a bed. BC211's shelter list was last updated at 7:30pm and never changes overnight or on weekends. Los Angeles built a live bed app, but a 2023 audit found its data was inaccurate because shelters didn't keep it updated. The problem isn't the map, it's the updating. So we made updating take one tap.
What it does
LuminestBC is a live shelter-bed network for Metro Vancouver.
- Tap Board: Front-desk NFC stickers for Bed freed, Bed filled, and We're full. Staff tap with any phone; there is no app install and no login. Mistaps can be reversed with a 10-second instant undo.
- Live Map: Counts update in sub-seconds across every screen, with 1-tap demographic filters (women-only, youth, pets, wheelchair accessibility) and color-coded freshness halos showing how recently staff confirmed each count.
- 60-Minute Holds: Workers can reserve a bed with an atomic row lock so the last bed can never be double-booked during transit.
- Voice Match & Hotline: Outreach workers can press the mic button on the live map, speak what they need in plain English, and hear the best matching open beds read aloud.
- Privacy by Architecture: Domestic-violence shelters never reveal coordinates, address, or staff phone numbers, and zero client PII (names, dates of birth) is ever stored.
How we built it
- Frontend: React 19 + Vite + TypeScript with Tailwind CSS and MapLibre GL for live map clustering.
- Backend & Realtime: Python 3.12 + Flask + Socket.IO broadcasting updates in under 300ms.
- Database: PostgreSQL + TimescaleDB on Tiger Data, logging every bed change and hold in an immutable
availability_eventstime-series hypertable with row-level locks (SELECT FOR UPDATE). - Voice & AI: Twilio for inbound calls, Web Speech API for in-browser speech, Gemini 2.5 Flash to extract structured JSON criteria, and ElevenLabs for streaming spoken audio.
- Deterministic Ranking: Plain Python applies hard eligibility rules and ranks real beds without hallucination:
score = distance_km + min(staleness_mins / 30, 10)
where distance_km is distance to the shelter and staleness_mins is minutes since staff confirmed the count. Lowest score wins.
Challenges we ran into
- Double taps and spotty cell signal: Solved with cryptographic one-time tap IDs and a 5-second tap debounce.
- Two workers racing for the last bed: Solved with atomic database row locks in PostgreSQL transactions.
- Dead air during voice generation: Callers and web users hear pleasant conversational audio while the matcher and TTS stream.
- Four people, one repo, 24 hours: Coordinated fast branch merges, small PRs, and strict boundary separation between frontend and backend.
What we learned
Keeping data fresh is harder than building the map. And while AI is extraordinary at turning messy natural speech into structured criteria, decisions about where a vulnerable person sleeps tonight belong in plain, deterministic, and testable code.
What's next for LuminestBC
- A field pilot with shelter staff and outreach teams across Metro Vancouver.
- Automatic SMS nudges to shelters whose pin freshness exceeds 3 hours.
- Multilingual voice support for non-English speaking callers.
- Extreme weather alert overlays triggered automatically by Environment Canada cold warnings.
Built With
- css
- data
- elevenlabs
- flask
- flask-socketio
- gemini
- github
- gunicorn
- leaflet.js
- maplibre
- nfc
- postgresql
- pwa
- pytest
- python
- react
- render
- socket.io
- tailwind
- tiger
- timescaledb
- twilio
- typescript
- vercel
- vite

Log in or sign up for Devpost to join the conversation.