This guide runs the whole stack on your machine, the app plus a self-hosted Supabase backend, and walks through the core calls. You need Node 20+, pnpm and Docker.
-
Start the app
From the repository root:
pnpm install pnpm setup:env # writes .env with fresh secrets pnpm supabase:up # Postgres, Auth, PostgREST, gateway, Studio pnpm dev # http://localhost:3000, migrates and seeds on startWait for
[bootstrap] readyin the dev server output, then check it’s healthy:curl http://localhost:3000/api/health # {"ok":true} -
Read the map
List every approved spot. The seed data adds 21 sourced spots around Kampala.
curl -s http://localhost:3000/api/spots | jq '.spots[0] | {id, title, severity, status}'{ "id": 8, "title": "Nansana intersection", "severity": 5, "status": "confirmed" }Filter by category, or by how recently spots were reported:
curl -s "http://localhost:3000/api/spots?category=boda_gang&since=30" -
Report a spot
curl -s -X POST http://localhost:3000/api/spots \ -H "Content-Type: application/json" \ -d '{ "title": "Phone snatching at the taxi stage", "lat": 0.3125, "lng": 32.5770, "area": "Old Taxi Park", "category": "robbery", "severity": 3, "time_of_day": "any" }'const res = await fetch("http://localhost:3000/api/spots", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: "Phone snatching at the taxi stage", lat: 0.3125, lng: 32.577, area: "Old Taxi Park", category: "robbery", severity: 3, time_of_day: "any", }), }) const { spot, pending } = await res.json()import requests res = requests.post("http://localhost:3000/api/spots", json={ "title": "Phone snatching at the taxi stage", "lat": 0.3125, "lng": 32.5770, "area": "Old Taxi Park", "category": "robbery", "severity": 3, "time_of_day": "any", }) spot, pending = res.json()["spot"], res.json()["pending"]The response is
201with the new spot and apendingflag. It starts asunverifiedwith one confirmation: yours. Ifpendingistrue, the site holds reports for moderator review and the spot isn’t public yet. -
Confirm it
Voting
1means “still dangerous”, and-1means “not anymore”. Replace22with your spot’s id.curl -s -X POST http://localhost:3000/api/spots/22/vote \ -H "Content-Type: application/json" -d '{"value": 1}'Voting the same way twice returns
409, because each visitor gets one vote per spot. Three net confirmations turn a spotconfirmed. See Core concepts. -
Search
Search is fuzzy and real time. Places come back with the mapped spots near them:
curl -s "http://localhost:3000/api/search?q=Mulago" \ | jq '.places[0] | {name, nearby: [.nearby[] | {title, m: (.distance_m|round)}]}'{ "name": "Mulago", "nearby": [{ "title": "Kalerwe", "m": 1240 }] } -
Check a route
Kalerwe to Nansana:
curl -s "http://localhost:3000/api/route?from=0.34918,32.57195&to=0.36582,32.52923" \ | jq '.routes[] | {minutes: (.duration/60|round), dangers: [.dangers[].spot.title]}'{ "minutes": 9, "dangers": ["Kalerwe", "Nansana intersection"] }
Next steps
- Browse the full API reference, with generated curl, TypeScript and Python samples.
- Read Errors and limits before shipping.
- Use the Recipes for route checks and nearby alerts in your app.