The problem
When someone who depends on accessible routes meets a broken ramp or a missing curb cut, reporting it means a municipal form: describe the problem, submit, wait, hear nothing. Nobody else finds out. There is no way to signal that a barrier is still there, or to confirm that someone fixed it, so one obstacle gets reported by ten people who never learn about each other — and each of them files it from a place they physically go, which is its own disclosure. I met this gap working with the vision loss community. It is the reason for the app.
The approach
I chose React Native and Expo to force myself to learn cross-platform development while shipping something real. Supabase handled auth and the database so I could focus on the domain problem instead of infrastructure. The pivotal call was implementing Postgres Row Level Security (opens in new tab) from day one — users can only see and edit their own reports. That wasn’t an afterthought; it shaped every feature that followed.
The hardest part wasn’t the code. My first designs missed what users actually wanted. They didn’t just want to report problems — they wanted to verify them, share workarounds, celebrate fixes together. That insight changed everything.
anonymous flag on it: a signed-in report and an anonymous report reach Postgres through separate code, and Row Level Security enforces both. A flag is a value a later read can forget to check; a second path cannot be entered by accident.Where it stands
A map-first mobile app where accessibility barriers are pinned to real city locations: report one, verify someone else’s, follow up when it gets fixed. The web build (opens in new tab) is browsable today. The iOS app has not shipped — App Store submission is the current work. One early TestFlight build reached a single outside tester; nothing has been submitted for review.
So there are no users yet, and no outcomes to report. What exists instead is evidence: a test suite in the low thousands, with the exact figure and the date it was measured receipted below, and a four-wave simulator walk that produced 48 findings and accounted for every one of them. What the privacy model does, and why it is shaped that way, is below.
My role
Anonymous reporting gets its own write path rather than an anonymous flag on the shared one. A flag is a value some later code path can forget to read; a second path cannot be entered by accident. The cost is that two paths drift, and they have — I take that trade because drift shows up in review and a missed flag does not.
The photo scrubber fails closed: if the location data cannot be verified gone, the upload aborts. The alternative was to carry on with the original bytes, which is what an earlier version silently did — the library call it trusted returns nothing at all, so the “stripped” result was always undefined and the untouched buffer went up. For an app where people photograph places they physically go, a false rejection is an annoyance and a false acceptance is a disclosure.
Effectively all of the implementation, the whole test suite, and most of the diagnosis, across more than 1,700 commits. The simulator walk above was theirs too — they found what it found.
I gate what lands, and I read the failures rather than the summaries. When an audit reports 48 findings, the four I read first are the ones it closed as “not a fix — the premise did not hold,” because a process that cannot report a negative is not a process.
What went wrong
A simulator walk found the flag detail sheet’s Report button rendering, looking enabled, taking the tap, and doing nothing. It is the Apple 1.2(b) objectionable-content sheet, and that screen is its only mount point in the whole app, for flags and for comments — so the report mechanism of a product built on things strangers submit was dead in every path, and would have shipped that way.
It had unit tests. They passed. They mounted the sheet on its own, and the defect was in where the parent mounted it, which a child’s test cannot see by construction. Four comments in the code asserted the broken arrangement was proven practice, each citing another, and the one citation that pointed outside the ring pointed at a screen where that arrangement is correct for a reason that did not apply here.
The structural fix was one line: a closing tag moved from before the pair of sheets to after them. The comments were rewritten as part of the bug, because they were the reason it kept coming back — the repo had already solved this same iOS error twice, and the correct pattern was sitting three lines above the broken one in the same file. The replacement test renders the parent instead of the child, and it was checked the only way that means anything: it fails against the old arrangement.


The Submit affordance this defect made unreachable — every path to it now proven by a test that fails against the old arrangement.
Reflection
Building this reinforced something I already believed: privacy isn’t a feature you add later — it’s a foundation, especially when the people using your app are sharing vulnerable information about the barriers they face every day.






