CityFlow
A full-stack web application for managing city infrastructure assets, work orders, and citizen service requests. React frontend, Node.js + Express API, PostgreSQL database, Leaflet map, and an experimental AI-assisted triage feature built on the Anthropic Claude API. Built as a way to learn how platforms like Cityworks shape municipal operations under the hood.
See it live →Walkthrough
Asset map, service-request intake, AI-assisted triage (experimental), and the KPI dashboard.
The question
Before writing a single line of code, the question worth sitting with: what is the simplest shape of a system that actually helps a small municipal operations team? Not a full Cityworks replacement, not an enterprise suite — the minimum viable surface that lets someone log a pothole, route it to the right person, and close the loop.
Four primitives kept coming back: assets (the things the city maintains), work orders (planned and reactive work against those assets), service requests (citizen intake — the raw, often-unstructured signal that becomes work), and staff (who does the work and who supervises). Everything else — the map, the dashboard, the AI triage — sits on top of those four.
Key decisions
Free tiles, no API key, no usage limits to plan around. The map is a primary surface and shouldn't depend on a vendor relationship for a demo.
LLM classification is non-deterministic, lacks an audit trail, and raises PII concerns under FIPPA. The feature is framed in the UI as experimental — a real deployment would need human-in-the-loop review on every suggestion and a self-hosted model for data residency.
At this scale, created_at / updated_at / resolved_at per row plus a status history log is enough. A separate audit table is the right call at production scale; for v1, it's premature.
A single Admin user is hardcoded in the sidebar — auth is deliberately deferred. Adding auth before the rest of the system is real felt like the wrong order.
What I built
Four layers, plain and load-bearing. The frontend renders maps and lists; the API does business logic and validation; the database is the source of truth; the Anthropic API is consulted only for the AI triage suggestion, and only as a separate code path.
Four boxes, one direction. The AI integration is intentionally drawn as a side branch off the API rather than between the user and the database — it's a feature you can disable without breaking the rest of the system.
The AI step is bracketed: it never writes to the database directly, and it never advances state on its own. A human reviewer at step 5 is mandatory before a work order is created.
What I'd change
Auth0 or Google OAuth for v1.5. SSO with the City's identity provider would be the production target. The hardcoded admin user goes away.
Anthropic Claude was the right call for this build, but a real deployment needs a model running inside the City's data residency. Llama or Mistral, hosted on internal infrastructure.
Currently the AI suggestion appears inline on each service request. A queue of pending suggestions with confirm/override buttons would make the human-in-the-loop loop tighter and easier to audit.
Inline audit columns are fine for v1. A separate audit_log table — one row per state transition with actor, timestamp, before/after — is the right call at scale.
Right now the frontend polls. SSE would push status changes to all connected clients — useful when multiple operators are working the same work order.
Today the seed data is hand-crafted. A nightly job that pulls fresh data from opendata.london.ca, normalizes it, and merges into the database would make the system feel less like a prototype and more like a working tool.