Case study · AI & LLMs · 2 min read
CivicPulse
Turn one photo of a local problem into a categorised report, a ready-to-send complaint letter and a pin on a community map.
- Role
- Full-stack: React front end, FastAPI back end, Gemini integration
- Type
- AI & LLMs
- Stack
- React, FastAPI, Gemini Vision, Firebase, Leaflet

- 11issue categories
- 3severity levels
- 1 photois all a report needs
- 0credentials needed for CI
01
Problem
Potholes, broken streetlights and dumped rubbish rarely get reported, because reporting them is a hassle.
CivicPulse makes it one step. You upload a photo, drop a pin and add a line of description. The app works out the rest.
Even when people want to report something, they don't know which department handles it or how to write a complaint that gets taken seriously. And there's no easy way to see whether a neighbour has already flagged the same thing.
02
What I did
- Gemini Vision reads the photo alongside the description, puts the issue into one of eleven categories and scores its severity as low, medium or high.
- The category is mapped to the responsible department, for example potholes to Roads and Infrastructure, or leaks to Water and Sewerage. A formal complaint letter is drafted with a timeline that depends on severity.
- Reports are stored in Firestore with the photo in Firebase Storage, and plotted on a Leaflet map with colour-coded severity pins. Other residents can upvote to confirm an issue, and a dashboard shows totals, resolution rate and breakdowns by category and severity.
Decisions along the way
- Keep the department routing in code
- Gemini decides the category and a plain lookup table decides the department. That keeps the routing predictable and easy to change, rather than leaving it to the model each time.
- Fail soft when the AI is unavailable
- If Gemini is down or the key is missing, the back end still saves the report with category "other", medium severity and a template letter. A report is never lost because a model call failed.
- Tests that need no keys
- The back-end tests stub Gemini and Firestore, so CI can run them and build the front end without any credentials.
Stack
- Front end
- React 19, Vite 8, Tailwind CSS 4, React Router 7, react-leaflet
- Back end
- Python, FastAPI
- AI
- Google Gemini via the google-genai SDK
- Data
- Firebase Firestore and Storage
- Deploy
- Docker Compose locally, Google Cloud Run
03
What I found
- An LLM is most useful for the fuzzy step (what's in this photo?) and least useful for rules you can write down yourself.
- Designing the fallback path first made the whole app easier to test.
04
What I'd do next
- Add user accounts and moderation, so reports and upvotes can be trusted.
- Host a demo so people can try it without their own keys.
05
Limitations
- There's no hosted demo. Running it needs your own Gemini API key and Firebase project.
- No user accounts or moderation yet, so anyone can report and upvote.
- CI runs the back-end tests and the front-end build. It doesn't call Gemini or Firebase.