A hackathon project exploring a practical question: how should the same urban barrier affect routes for people with different accessibility needs? Built for the Tijuana Sin Barreras track at HackFox 2026 by team Entropyc.
Senda separates a barrier's physical type from its effect on six functional profiles, combines selected profiles using the worst case, and recalculates an active route when a relevant citizen report arrives.
Prototype, not a validated navigation product. Neither route safety nor accessibility has been independently validated. Demo coordinates, reports and geometry are synthetic; do not use them to make navigation decisions.
Actual local application, with a schematic route diagram rather than a street map. No Google Maps credentials or GPS are used.
Requirements: Python 3.12, Node 22, Bun 1.3.14. Run from the repository root in two terminals. Installation downloads dependencies; the running synthetic workflow needs no cloud services.
# Terminal 1 — local API, volatile synthetic data
python3.12 -m venv apps/api/.venv
apps/api/.venv/bin/pip install -r apps/api/requirements.lock
cd apps/api
ROUTING_MODE=synthetic FIRESTORE_ENABLED=false SEED_FIRESTORE=false \
GOOGLE_MAPS_API_KEY= GEMINI_API_KEY= \
.venv/bin/uvicorn main:app --host 127.0.0.1 --port 8080 --no-proxy-headers# Terminal 2 — local frontend
cd apps/web
bun install --frozen-lockfile
NEXT_PUBLIC_API_URL=http://127.0.0.1:8080 \
NEXT_PUBLIC_DEMO_MODE=1 NEXT_PUBLIC_GOOGLE_MAPS_API_KEY= \
NEXT_TELEMETRY_DISABLED=1 bun dev --hostname 127.0.0.1Open the local demo. NEXT_PUBLIC_* values are baked into production builds: rebuild when changing modes. Explicit environment values above override any local .env credentials.
- Focus Buscar destino, enter Zona Rio, and keep the explicit demo origin Centro. Silla de ruedas is selected. Press Buscar.
- Add Ceguera and choose Actualizar vista previa, then Iniciar. The diagram, distance, steps and barrier lists reflect the new route. Endpoint barriers may remain: visible warnings are intentional.
- Expand route details for Pasos and Barreras. Leer is optional; every instruction is also available as text.
- Press the red report button. Choose Usar punto sintético de la ruta, which fills a point on the active route and selects a missing-ramp report. Submit with Enviar reporte.
- The local API stores and broadcasts the synthetic report. The active route recalculates, announcing the result or remaining blockers. A second browser tab receives the same report through SSE.
- Cancelar (deshacer) removes a recent report only after the API confirms success. Terminar viaje ends the route. Restart the API to reset demo data.
Use Centro, Zona Rio, CECUT, Plaza Rio, Otay, UABC Otay, Aeropuerto, La Mesa, Hospital General, Estadio Caliente, Playas, Garita Chaparral, Parque Teniente Guerrero, Av Revolucion, or Macroplaza. Matching ignores case/accents but does not guess partial names. Both route inputs also accept latitude, longitude, e.g. 32.5331, -117.0382.
| Area | Implementation |
|---|---|
| Profile effects | Six profiles; barrier worst case B > D > L > ·; amenities use strongest relevance CLAVE > UTIL > ·. Attribute rules for slope, width and crossing facilities. |
| Routing adapter | Valhalla pedestrian requests with profile-aware exclusion candidates, explicit coordinate conversion and meter/minute units. Contract-tested with mocked responses; real tiles not exercised in this audit. |
| Synthetic router | Deterministic sampled geometry with attempted B/D detours. No street graph, sidewalk knowledge or guarantee a detour clears every feature. |
| Route classification | Distances to segments of the returned polyline, not just vertices or the direct origin/destination line. All known B barriers on the returned path are checked, including endpoints and large detours. |
| Reports and live updates | Validated multipart reports, retry IDs, local SSE snapshots/events, reconnect reconciliation, duplicate suppression and stale-request protection. |
| Storage | In-memory by default, reset on restart. Optional Firestore persistence, explicit opt-in and surfaced failures. Designed for one API process. |
| Map | Credential-free schematic in demo mode. Google Maps JS in real mode requires a browser key and may incur charges. Text route steps and barrier lists are available without it. |
| Accessibility | Keyboard-operable controls, visible focus, report-dialog focus containment/restoration, status announcements, text scale, contrast/reduced-motion preferences. Speech and vibration are optional. No formal WCAG or assistive-technology certification. |
| Seed and reports | Seed is synthetic_seed; synthetic-mode reports are synthetic_report; real-mode reports are citizen_report_unverified. No automatic promotion to verified observations. |
| Other hackathon screens | Government aggregates and transport views are demonstrations. Vision classification, moderation, community verification and transport usability scoring are not implemented. |
- B (blocking): eligible barriers in the origin/destination bounding box padded by 0.018° become exclusion candidates. D (difficult): candidates must also be less than 250 m from the direct corridor. L and neutral effects do not change route selection.
- Candidates within 40 m of an endpoint are not excluded, to avoid excluding the starting/ending road. The 50-location engine limit prioritizes B before D, then corridor distance and ID. These bounds are prototype heuristics, not accessibility standards.
features_evitadasmeans selected corridor candidates more than 35 m from the returned path. It does not establish that those candidates would have been encountered on a baseline route, or prove causal avoidance. No baseline comparison is performed.features_bloqueantes_en_rutaincludes all known B barriers within 35 m of the returned path, even if they were outside the candidate box, capped out or near endpoints. Nearby parallel streets can produce false positives.features_aprovechadasincludes profile-relevant amenities within 50 m of the path; this does not prove the amenity entrance is reachable.- Live rerouting uses a deliberately wider 80 m trigger for new B reports. It reuses the committed origin, destination and profiles, rather than requesting GPS again. It is not continuous turn-by-turn tracking.
- Valhalla exclusions snap points to graph edges. D is approximated as exclusion, not a graduated penalty. Native wheelchair costing takes precedence when selected; blind costing is used otherwise for BLIND. Multi-profile matrix effects still combine across every selected profile; combined wheelchair/blind requests do not receive blind-specific native narration.
- A rejected constrained search is retried once without exclusions, clearly marked
exclusions_applied=false. Remaining blockers stay visible even when route details are collapsed. A returned path is a proposal, not confirmation of suitability.
GPS denial, unknown/partial/out-of-area geocoding results, unavailable routing and empty routes produce errors. A failed reroute retains the previous path with a failure announcement. Ending a trip or issuing a newer request invalidates older responses. No hidden location or fabricated real route replaces the request.
flowchart LR
UI[Origin + destination + profiles] --> API[FastAPI route adapter]
API --> Matrix[Worst-case profile effects]
Matrix --> Engine[Synthetic router or Valhalla]
Engine --> Audit[Returned-path barrier and amenity checks]
Audit --> Display[Diagram/map + text + warnings]
Report[Citizen report form] --> Validate[Validation + persistence]
Validate --> SSE[SSE snapshot/events]
SSE --> Decision[Active route + B effect + distance + dedupe]
Decision --> API
apps/web: Next.js 15, React, TypeScript, Tailwind, Zustand; route/report UI and live-update lifecycle.apps/api: FastAPI, Pydantic, geospatial utilities, matrix, synthetic router, Valhalla adapter and report store.data/fixtures: shared matrix examples consumed by Python and TypeScript tests.services/valhalla: local engine configuration; tiles and image are external prerequisites.- Architecture and contracts, product specification, audit evidence and limitations.
This is a separate integration path; the synthetic demo does not validate it.
- Follow Valhalla setup: choose an immutable engine image, obtain an OSM extract and build compatible tiles locally. No tiles are committed. Record the image digest and extract checksum.
- Run the API with
ROUTING_MODE=valhalla,VALHALLA_URL=http://localhost:8002, andFIRESTORE_ENABLED=false. Coordinates need no geocoder. Text inputs requireGOOGLE_MAPS_API_KEYwith Google Geocoding enabled; requests may be billable. - Run/rebuild the web app with
NEXT_PUBLIC_DEMO_MODE=0, the local API URL and, optionally,NEXT_PUBLIC_GOOGLE_MAPS_API_KEYfor the street map. Without a Maps key, use textual route details. - Firestore is optional:
FIRESTORE_ENABLED=true,FIREBASE_PROJECT_IDand Application Default Credentials are required. Initialization/read/write failures do not silently switch to memory.FIREBASE_CREDENTIALS_JSONis a legacy unused setting; use ADC. Seed writes require separateSEED_FIRESTORE=trueopt-in. Do not use that setting against existing live data.
Directions use Valhalla's supported es-ES locale directly; routing does not call an LLM. See the Valhalla API reference for native costing and exclusion semantics. Historical deployment notes are in infra/deploy.md; deployment is unnecessary for portfolio review.
# From repository root
apps/api/.venv/bin/pip install -r apps/api/requirements-dev.txt
apps/api/.venv/bin/python -m pytest -q apps/api/tests
cd apps/web
bun run typecheck
bun run lint
bun run test
bun run build
# Browser test starts its own local API and frontend; ports 8080/3000 must be free.
bunx playwright install chromium
bun run test:e2e
# Optional: refresh the README screenshot from the tested UI
CAPTURE_DEMO=1 bun run test:e2eCI installs the frozen web lockfile and Python lock, then runs API tests, web typecheck/lint/unit tests/build and the synthetic browser walkthrough. External API responses are mocked; the browser walkthrough rejects external requests and unexpected GPS calls. Python tests forcibly disable Firestore and cloud API keys.
Report defenses include finite, regional coordinates; kind/subtype allowlists; 500-character descriptions; validated JPEG/PNG/WebP images (16 MP max); 6 MB whole-body, 5 MB upload and ~450 KB encoded-photo limits; per-peer rate limits; a 1,000-report process cap; and retry-ID conflict detection. SSE clients and queues are bounded; overflow forces a fresh snapshot. Undo persistence failures retain the report and show an error.
This remains a local portfolio prototype, not a hardened public service. Report/delete endpoints have no ownership authentication or moderation. An attacker knowing a recent report ID can delete it. Rate limits and SSE are process-local; configure trusted proxy handling explicitly and never trust arbitrary forwarded headers. Firestore writes are synchronous, cross-process broadcasts and cross-instance idempotency are not implemented, and externally edited Firestore rows are not watched. Authentication, moderation, shared rate limiting and a proper distributed event/persistence layer are prerequisites for public deployment.
Jorge Alejandro Sandoval Romo — software development: the web experience, API integration, accessibility-profile model, routing/report workflow and deployment setup. This portfolio revision adds reproducible local execution, regression tests and explicit implementation limits. Bernardo Morales contributed co-ideation and field research. The matrix and UI have not yet been validated with disabled participants.
Next steps before use beyond a demo: participatory field validation, a documented real-tile integration run, accessibility testing with assistive technologies, and authenticated/moderated reports.
Jorge Alejandro Sandoval Romo · Bernardo Morales
MIT — LICENSE. Real routing data: © OpenStreetMap contributors, subject to ODbL attribution requirements.
