Case study · FieldProof
Offline-capable field
inspection workflow
Field work that survives bad connectivity. An inspector records findings on a site with no signal; the work syncs once there is one, freezes when it is handed in, and an administrator reviews it — photographs and all — from a hosted console that can write nothing at all.
- CI
- TESTS
- SOURCE
- LIVE DEMO
- RELEASE
- DEVICE QA
- DATABASE
- SECURITY
- DESIGN
Review console ↗
Live, with published credentials
Sign in as the admin and as the inspector. The second one is refused — that refusal is the product.
Android build ↗
v0.1.0-demo · sha256 published
Sideload it. Android will warn about an unknown developer, as it does for any self-signed build.
1. Problem
Inspections happen where the connectivity is not. An inspector has to record structured findings and attach photographs on a site with no signal, and the record has to survive the walk back. Meanwhile the office needs submitted work to be searchable, reviewable and immutable once it has been handed in.
The interesting part is not the forms. It is what happens at the boundaries: what an unsynced draft is allowed to claim, what a retry is allowed to create twice, and what one inspector is allowed to see of another's work.
2. Built workflow
- 01Inspectionsite, address, client
- 02Findingsitems and severity
- 03Photo evidenceprivate bucket
- 04Submitserver stamps and freezes
- 05ReportPDF from one snapshot
- 06Historyfull-text search
- 07Admin reviewread-only console
3. Mobile
Flutter · Android verified, iOS not claimed






These are rendered from the real widget trees at 390×844, with the platform fonts registered — not mockups, and not photographs of a phone. They are not a substitute for device QA, which was done separately on two handsets.
4. Admin
Next.js · deployed · read-only by policy



Captured against the live demo project while signed in as the published reviewer, so these are a point-in-time snapshot of real data rather than a fixture.
5. Offline correctness
The chain as it actually stands, including the link that is missing
- Create a draft with the server unreachableDriven through the widget tree with the remote repository failingVERIFIED
- Add, edit and delete its punch itemsSame editor as online, no separate offline code pathVERIFIED
- Attach a photograph offlineNot implemented. Deferred as its own slice, with the reason recordedUNAVAILABLE
- Force-stop and reopen, still offlineThe queue is rebuilt from stored bytes aloneVERIFIED
- Reconnect and push the queueAlso proven against the real project, not only against fakesVERIFIED
- Replay the push after a retryUpsert on a device-generated key — one inspection, two items, no duplicateVERIFIED
- Submit only after the server has agreedAn unsynced draft raises rather than looking submittedVERIFIED
Marked synced is a deletion, not a flag: the local record is removed in one place, after the server has been read back. The slice landed as a reviewed pull request ↗
6. Security and data boundary
Enforced in the database, so removing the application gate leaks nothing
Denied by absence, not by rule
RLS is enabled and forced on every application table, and no policy exists for the anonymous role. An unauthenticated client is refused outright rather than returned an empty list.
Administrators cannot write
Every admin insert, update and delete is refused by the database. Updates and deletes deny silently, so each test re-reads the row afterwards to prove nothing changed.
Role cannot be self-escalated
The role column is revoked from authenticated users at the column level. A user can rename themselves and cannot promote themselves.
No privileged key reaches a client
The console refuses to start unless the key is positively identifiable as publishable, and CI greps the built bundle to prove no privileged key shipped.
The Flutter tests deliberately prove nothing about RLS — their fakes do not re-implement policy, because a fake that enforced it would be testing itself. That is what the pgTAP suite is for ↗
7. Verification
Every row opens the artifact behind it, or says why it cannot
| Claim supported | Status | Evidence |
|---|---|---|
Offline capture, sync, reporting and search behave as specified Flutter unit and widget suite — 279 tests passed | VERIFIED | Open ↗ |
Every push is gated on format, static analysis and the test suite dart format --set-exit-if-changed, flutter analyze --fatal-infos, flutter test | VERIFIED | Open ↗ |
One inspector cannot read or mutate another's work at any level of the chain pgTAP suite — 183 assertions across 9 files, run three times per CI pass. Covers silent denials, not only raised errors | VERIFIED | Open ↗ |
Migrations apply from an empty database to head 5 migrations applied unseeded, then re-applied seeded, against a real Postgres engine | VERIFIED | Open ↗ |
The security boundary holds against the real hosted project Hosted Supabase smoke — 38 tests through real Auth, real RLS and real Storage, not fakes | VERIFIED | Open ↗ |
The review console is read-only and scoped to submitted work Vitest — 41 tests across 6 files, plus lint and tsc --noEmit | VERIFIED | Open ↗ |
The review console is deployed and publicly reachable Live deployment with demo credentials published deliberately, so the security model can be pushed against | VERIFIED | Open ↗ |
An installable Android build exists and is downloadable v0.1.0-demo — fieldproof-f12d71d.apk, sha256 published alongside the tag | VERIFIED | Open ↗ |
The offline workflow was walked end to end on real hardware Two Android devices (SDK 30 and SDK 36) against the hosted project. The pass found a real defect — the app had no submit action at all | VERIFIED | Open ↗ |
Sixteen screens were rendered from the real widget trees 390×844 @2x goldens with the platform fonts registered. Not a CI gate, and explicitly not a substitute for device QA | VERIFIED | Open ↗ |
Console screens captured against live demo data Headless Chrome, signed in as the published demo reviewer. A point-in-time snapshot, not a reproducible fixture | VERIFIED | Open ↗ |
The whole system is public and inspectable Mobile, admin, migrations, RLS suite, CI and the acceptance record | VERIFIED | Open ↗ |
iOS build verification Passed once on a macOS runner at c796b6f. Moved to manual dispatch and not re-verified at HEAD, so iOS is not claimed | PARTIAL | Open ↗ |
Offline photo capture Not implemented. Drafts and punch items work offline; attaching a photo does not. Deferred as its own slice rather than quietly dropped | UNAVAILABLE | Open ↗ |
Dependencies & limitations
Stated here rather than left for a client to discover.
- —A portfolio project. There is no customer deployment, no production user volume, and none is implied.
- —The hosted project holds demo data only. Both demo accounts are published deliberately, so the security model can be pushed against rather than described.
- —Offline photo capture is not implemented. Drafts and punch items work offline; attaching a photograph does not.
- —Opened cold while offline, the history takes roughly thirty seconds to render — it waits for a profile lookup to fail. Latency, not data loss.
- —iOS is unverified. The build passed once on a macOS runner and has not been re-run at the current head, so no iOS claim is made.
- —Not published to any app store, and no store listing is claimed.
- —Deployment of the console is manual. The Vercel account is not linked to the identity that authors the commits, so Git-triggered builds are off.