Skip to content
Huy Tran

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.

PORTFOLIO PROJECT

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

  1. 01Inspectionsite, address, client
  2. 02Findingsitems and severity
  3. 03Photo evidenceprivate bucket
  4. 04Submitserver stamps and freezes
  5. 05ReportPDF from one snapshot
  6. 06Historyfull-text search
  7. 07Admin reviewread-only console

3. Mobile

Flutter · Android verified, iOS not claimed

FieldProof mobile — History
Historynewest first, total order
FieldProof mobile — Offline
Offlinenot synced, and says so
FieldProof mobile — Findings
Findingsseverity and open state
FieldProof mobile — Editor
Editorconstrained enums
FieldProof mobile — Report
Reportsubmitted work only
FieldProof mobile — Search
Searchsite, address, client

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

FieldProof admin console — Submitted queue
Submitted queuethe SMOKE … do-not-keep rows are real — the hosted smoke test writes them into the demo project on every CI run
FieldProof admin console — Review detail
Review detailphotos through short-lived signed URLs
FieldProof admin console — Search
Searchone term matching a site and a client

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

  1. Create a draft with the server unreachableDriven through the widget tree with the remote repository failing
    VERIFIED
  2. Add, edit and delete its punch itemsSame editor as online, no separate offline code path
    VERIFIED
  3. Attach a photograph offlineNot implemented. Deferred as its own slice, with the reason recorded
    UNAVAILABLE
  4. Force-stop and reopen, still offlineThe queue is rebuilt from stored bytes alone
    VERIFIED
  5. Reconnect and push the queueAlso proven against the real project, not only against fakes
    VERIFIED
  6. Replay the push after a retryUpsert on a device-generated key — one inspection, two items, no duplicate
    VERIFIED
  7. Submit only after the server has agreedAn unsynced draft raises rather than looking submitted
    VERIFIED

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

Evidence supporting each claim, with verification status and link.
Claim supportedStatusEvidence
Offline capture, sync, reporting and search behave as specified
Flutter unit and widget suite — 279 tests passed
VERIFIEDOpen ↗
Every push is gated on format, static analysis and the test suite
dart format --set-exit-if-changed, flutter analyze --fatal-infos, flutter test
VERIFIEDOpen ↗
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
VERIFIEDOpen ↗
Migrations apply from an empty database to head
5 migrations applied unseeded, then re-applied seeded, against a real Postgres engine
VERIFIEDOpen ↗
The security boundary holds against the real hosted project
Hosted Supabase smoke — 38 tests through real Auth, real RLS and real Storage, not fakes
VERIFIEDOpen ↗
The review console is read-only and scoped to submitted work
Vitest — 41 tests across 6 files, plus lint and tsc --noEmit
VERIFIEDOpen ↗
The review console is deployed and publicly reachable
Live deployment with demo credentials published deliberately, so the security model can be pushed against
VERIFIEDOpen ↗
An installable Android build exists and is downloadable
v0.1.0-demo — fieldproof-f12d71d.apk, sha256 published alongside the tag
VERIFIEDOpen ↗
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
VERIFIEDOpen ↗
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
VERIFIEDOpen ↗
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
VERIFIEDOpen ↗
The whole system is public and inspectable
Mobile, admin, migrations, RLS suite, CI and the acceptance record
VERIFIEDOpen ↗
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
PARTIALOpen ↗
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
UNAVAILABLEOpen ↗

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.