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. Barriers are meant to be seen. A shared map is the whole point, but only the person who filed a report can edit its content; status changes like verifying it or marking it fixed are open to the community that shares the map. That wasn’t an afterthought; it shaped every feature that followed.
Inside the build
See it in motion.
From map pin to report form, the current reporting flow in motion. 
Reporting a barrier: quick-fill templates, category, severity, and description. 
Community review: neighbours verify barriers and mark resolved reports.
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.
The map was the other hard part. To a screen reader a native map is one opaque view, so Flagstone keeps a parallel, linear list of nearby barriers that opens on its own when a screen reader is detected, and selecting a marker no longer drags focus back to the map. Text follows Dynamic Type up through the accessibility sizes, touch targets hold a 44-point floor, contrast is unit-tested against the WCAG ratio, and when Reduce Motion is on the map camera jumps instead of animating. The app is built and tested against WCAG 2.2 AA, with an accessibility check in CI that fails a pull request below its threshold.
Privacy set the remaining shape. Location is foreground-only: the app declares no background-location entitlement and calls no background-location API, a line the privacy review held when an unrelated fix tried to add the keys. Every submission, anonymous or signed in, runs the same content filter, and any flag or comment can be reported through the abuse-report sheet Apple requires. Photos are capped in size and scrubbed before upload. Accounts can be deleted, with the cascade handled by a tested pipeline. There is no analytics or tracking SDK in the app.
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, and follow up when it gets fixed. The web build (opens in new tab) is browsable today. The iOS app is available on the App Store (opens in new tab) in the United States and Canada: version 4.1.1, free, released September 15, 2026. It was submitted for review on August 31, 2026, and before that one early TestFlight build reached a single outside tester.
The launch is recent, so adoption and usage numbers are not yet meaningful, and I would rather say so than imply otherwise. They go here when there is real usage to report. What exists now is evidence: a test suite in the thousands, with a dated figure receipted below; a real-device simulator walk that produced 48 findings tracked across four repair waves, with fixes, partial fixes, outstanding device checks and deliberate scope decisions; and a privacy model whose shape, and the reasons for it, follow.
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,900 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 ones I read first are the ones it closed without a fix, 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.
This is the support work Flagstone demonstrates: tracing a failed interaction to its cause, choosing a repair around accessibility and privacy risks, checking the regression evidence, and deciding what is ready to release.
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.








