Inspiration
Sapelo Island is a ferry ride from the Georgia mainland. About forty people live there year-round, most of them in Hogg Hummock, one of the last intact Gullah-Geechee communities on the coast. There is no bridge, grocery store, fire station, or public works department. Cell signal also comes and goes depending on where you are on the island. In 2023, the county moved to weaken the zoning that had helped protect the community from being displaced, and residents have been fighting it since. One of the hardest parts of that fight is collecting reliable evidence.
Most civic reporting apps assume three things: people are available to receive reports, there is a reliable network connection, and there is a department responsible for handling the problem. Sapelo has none of those things. A 311 app without anyone to respond is basically a suggestion box that nobody empties.
The response capacity is already there, though. It is the residents. Someone has a boat. Someone has a generator, a saw, or a backhoe. That equipment and experience already exist in the community. What is missing is a way to see what resources are available, connect them to problems, and distribute the work fairly.
That is why capability became the main routing factor instead of department. We also avoided the obvious approach of sending each job to the nearest person. On a ten-mile island, always choosing the closest responder can cause the same few people to receive nearly every job in their area. Eventually, they stop responding. We believe mutual aid is more likely to fail from overworking the same people than from taking a few extra minutes to reach a problem.
Instead, our system looks at workload per person and does not use distance as a factor. That is one of the main ideas behind the project.
How We Built It
A resident can photograph a hazard and report it even when they do not have a reliable connection. The phone blurs the image and removes EXIF information before anything leaves the device. The report is then saved in IndexedDB with the time it was taken. When a connection becomes available, the report is uploaded.
Gemini classifies the photo using built-in hazard types along with any additional types the community has created. It assigns a severity level, gets the current tide information from the NOAA station near Sapelo, and checks for nearby reports using a PostGIS radius query.
The system then filters responders based on the capability required for the job. For example, a boat-related task is only sent to pods that have access to a boat. After that, the remaining pods are ranked based on their workload.
The final result is a job assigned to a capable pod, along with an Open311 export that can be read by county agencies.
We built the platform using Next.js, Supabase Postgres with PostGIS, Gemini through the Vercel AI SDK, MapLibre, and deck.gl. We use server components by default, which keeps the model key on the server. Row-level security is enabled by default on every table.
Photos are separated into two storage buckets. The blurred version is public and permanent, while the original is restricted and automatically deleted after thirty days.
The ranking system itself is simple. Staffed pods come first, followed by workload per person, queue depth, how long the pod has been idle, and finally the pod name to keep the ordering stable.
Capability matching happens before the ranking system. We did not simply add another weight to the ranking formula. A weight could allow a rested pod without the required equipment to outrank a busy pod that actually has what the job requires.
Hazard types and pod capabilities are stored in the database and connected to the community. If a community wants to track something new, such as oyster lease damage, it can add a new type without needing a new deployment.
Challenges We Faced
Offline Reporting
A resident who needs a signal to file a report may never file it. On Sapelo, that can happen often.
Background Sync is only supported consistently in Chromium-based browsers. Without another way to send queued reports, iPhone users could save reports and never have them uploaded.
We added a second flush path to handle this. Report IDs are created on the phone so reconnecting cannot accidentally create duplicates. The timestamp also comes from when the photo was taken rather than when the report reaches the server. Otherwise, a boardwalk that broke on Monday could appear to have been reported days later when the ferry finally provided a connection.
Bugs That Passed Every Check
We had a period where all of our checks were passing even though four major bugs were still in the system.
We started with twelve built-in hazard categories and allowed communities to add their own. Some lookup tables had originally been designed around those twelve categories. Once communities could create new categories, those tables were no longer complete.
TypeScript did not catch the problem because the key was coming from the database.
This caused several problems. An export could return a successful 200 response while leaving out 68 reports. A routing chart could show only potholes instead of the actual hazards on the island. Coastal reports could cause a 500 error. The worst problem was a report that saved successfully but created no job, meaning nobody was assigned to it even though the resident saw a normal thank-you screen.
Three of these problems were connected, so fixing one often exposed another.
Photo Blurring
The photo blur system is a compromise.
FaceDetector is not consistently available on iOS Safari, Firefox, or standard Chrome. Our fallback blurs the top and bottom thirds of the image, but that means a face in the center of the frame could still be visible.
Blurring the entire image would make the hazard harder to identify, which defeats the purpose of the report. We chose the tradeoff intentionally and documented the limitation instead of pretending the system was perfect.
Consent
Consent is something we cannot solve with code.
Sapelo's residents did not ask for this project. It is based on the public record of a real community, and any real deployment would require the community's consent and a way for residents to correct the information.
A demo that judges can open is not the same thing as deploying a system in Hogg Hummock. We wanted to be clear about that difference.
What We Learned
A successful build does not necessarily mean the product works. Every automated check passed while the four major bugs were still active. Having someone actually use the system caught problems that hundreds of automated tests missed.
Our end-to-end test also missed the biggest bug because it used a report without a photo. That report followed a different path and never reached the broken part of the system.
We also learned that data needs to be treated as flexible everywhere it is used, not just where it is created. Our classifier supported community-created categories, but several parts of the application still assumed that only the original categories existed.
We changed the system so it can accept any category key, check the built-in types, then check the community's custom types, and finally fall back to something readable instead of failing silently.
Another major lesson was that silent failures are dangerous. A successful response with no data, a saved report with no job, or a chart showing the wrong records can all look normal from the outside.
We now ask what a feature should look like when it fails while we are still designing it.
Fairness also needs to be understandable to the people using the system. A volunteer should be able to see why a job was assigned to one pod instead of another.
We show the ranking information directly. For example, one pod might have 159 minutes of work per person while another has 250. Distance is intentionally not included. This makes the decision easier for residents to understand and question.
We also added a manual dispatch option because capability matching can sometimes find nobody who can handle a job. Instead of silently dropping the task, the system makes that situation visible so someone can decide what to do.
Our demo video could not cover every part of the program or all of the code behind it. The linked GitHub repository contains the documentation and code needed to understand the full project and how we built it.
Built With
- 3d-printing
- ag-grid
- deck.gl
- edge-impulse
- esp32
- esp32-cam
- google-gemini
- gsap
- indexeddb
- maplibre-gl
- next.js
- open311
- playwright
- postgis
- postgresql
- pwa
- react
- sentry
- shadcn-ui
- supabase
- tailwindcss
- tensorflow-lite
- typescript
- vercel-ai-sdk
- vitest
Log in or sign up for Devpost to join the conversation.