Yuno15

Client workPWAOffline

A field app that works with no signal.

Technicians fill in their report on the roof, in airplane mode. The office gets it the same day and sends it to the client as a PDF.

Stack

  • PWA
  • JavaScript
  • Supabase
  • PostgreSQL
  • Python
  • FastAPI
  • Claude
The same dashboard at the same moment on a MacBook Pro in dark mode and an iPad in light mode
The same dashboard at the same moment: the office in dark mode on a Mac, the technician in light mode on an iPad. Screenshots of the app in production, demo mode: branding, places and data neutralised.
69 invariants
Tests each born from a production bug, reading the shipped code.
Same day
the office sees the job, instead of three days of re-typing before.
Airplane mode
for the whole report. Only the sync needs a network.
1 minute
between two mailbox reads by the agent. Every write waits for a human to approve it.

Interfaces

The phone on the roof, the dashboard at the office.

Both screens read the same database. What the technician validates shows up on the office map as soon as their phone is back online.

The technician's phone
The technician's phone
The office dashboard
The office dashboard
Left, the technician's home screen: the current job, the route, the next jobs. Right, the office: today's counters, crews on the map, the month planner. Screenshots of the app in production, demo mode. Places and data neutralised.

Review

The agent proposes, a person approves.

Signed quotes, invoices and claims arrive by email. The agent extracts the fields, each one stays editable, and nothing is written to the database until someone clicks the button on the right. Reconstructed interface, fictional data.

Features

From the roof survey to the client report.

Fifteen steps at most per report, and only the ones that apply to that type of job.

  • Offline first

    Every entry is written to the phone first and joins a local queue. Saving never waits for the network.

  • Field-level merge

    The offline technician and the live office edit the same record. A written rule decides who wins, and a conflict is always flagged.

  • Built-in camera

    Photos are taken in the app, through the browser. On the field's rugged Android phones, the system camera often refused to open.

  • Client report as PDF

    The office generates the report in one step, from the fields and photos of the survey.

  • Per-company isolation

    Every database row belongs to one company (PostgreSQL Row Level Security). A check verifies it with a real technician account.

  • Encrypted backups

    Encrypted off-machine copies, with a restore verified byte for byte. The app installs from the browser, no app store needed.

Architecture

The path of a report.

Only the fourth step touches the network. Everything before it works in airplane mode.

  1. 01 On the roof The technician fills in, photographs and validates the steps for that type of job.
  2. 02 Written to the phone Immediately: the record in local storage, the photos in a dedicated local database.
  3. 03 The queue The change waits its turn, with a revision number that changes on every edit.
  4. 04 Back online The queue replays in order. Before each write, it re-reads the server's row and merges field by field.
  5. 05 The office The map, today's counters and the planner update, and the client report comes out as a PDF.

Email agent

A read-only agent on the client database.

It reads two mailboxes every minute and prepares proposals. Its only allowed write: setting a job's status to “scheduled”.

What the agent does on its own

  • Read two mailboxes every minute and sort each message
  • Open attached PDFs and extract client, references, amounts and dates
  • Query the client's database, read-only
  • Draft a reply, left unsent

What it is not allowed to do

  • Write to the client's database
  • Send an email
  • Create a record nobody has reviewed
  • Delete or overwrite existing data

Production incidents

Four failures, and what they changed.

Each one left a written check that can be replayed.

01 · Lost draft

A technician lost a report they had already filled in

July, back from a job site

The assumption
The draft was saved on every entry and every photo. Coming back to the app should find it.
What happened
They left the app during a report. On return they were on the home screen, and the draft's photos were gone.
Why
Five defects chained together: a cache serving a stale version, photo storage that had become private, a race between two operations, expired image URLs and a fragile sync. The draft existed, but reopening it required a button they never saw, and the photo was still being written to the local database when they left the app.
What changed
Reopening the app on an active draft reopens the report at the saved step. The draft waits for its photos to finish writing before it is saved, and it is also saved when the app is left.
The proof
Invariant tests cover the chain by reading the shipped source. The live version is checked by downloading the deployed service worker.
02 · Invisible photos

Every photo disappeared, with no data lost

July, reported as “no photos on a job”

The assumption
Photo storage is private: the app signs temporary URLs to display them, and keeps a public URL as a fallback.
What happened
Every photo disappeared at once, for every user, with no error message.
Why
On private storage, a public URL returns an error: the fallback had never worked. All URLs were signed in one batched call whose failure was swallowed. A single transient network error emptied the whole display, while every file and every row was intact.
What changed
Valid signed URLs are kept across reloads, the display rebuilds from them at startup, and signing runs in small isolated batches: a failed batch only blocks its own photos.
The proof
A three-layer diagnosis, written and replayable: does the row exist, does the file exist, does signing work. Three yeses mean nothing is lost and the defect is in the display.
03 · Sync queue

An entry typed during a sync disappeared

July, found by an adversarial audit

The assumption
Send pending records one by one, then rewrite what remains in the queue. The code had run that way for months.
What happened
Anything typed during a sync could be lost. The server had one version, the screen another, and no send was left to close the gap.
Why
Read the queue, send, rewrite the queue: the network separates those three moments, and the rewrite erased entries added in between. The existing timestamp could not detect it, because it is deliberately kept on re-queue for conflict detection.
What changed
Every entry carries a revision counter that changes on every edit. On acknowledgement the queue is re-read, and a send's result is only applied if the revision has not changed. Whatever was added during the send stays in the queue.
The proof
The merge rule lives between two markers in the source, and a script tests exactly that zone: moving the code without moving the test makes the script fail.
04 · False success

A button said “done” and deleted nothing

Found while writing a test against the real database

The assumption
The database isolates data per company, row by row. The rules were written and reviewed.
What happened
A reset button reported success every time and removed no rows, for weeks. Nothing failed, so nobody noticed.
Why
When a delete matches no security policy, PostgreSQL returns no error: the query succeeds and deletes zero rows. From the app, a silent refusal looks exactly like success.
What changed
Isolation is checked with a real technician account on the real database: the check creates a throwaway record and tests what that account can and cannot reach.
The proof
The check runs with a technician's rights and fails if a rule is missing, the very case it was written for.

Product decisions

Two diagnoses settled by a measurement.

Before changing the code, measure what is actually happening.

Duplicate jobs

Measured

0 of 55

duplicate IDs, and zero shared IDs across 181 calendar events. Deduplication was working.

Decided

Upstream

The duplicates came from calendars where an appointment deleted and re-created gets a new ID. The fix was made at the source; the merge did not change.

Test coverage

Measured

3 areas

tested: startup, service worker, offline. A total block at step 5 went unnoticed for weeks.

Decided

Input = output

A check verifies that what is entered comes out in the report. When it was added, it found five defects in one pass.

Role
Product design, field UX, full-stack development
Context
Client work · anonymized · 2026
Result
69 invariant tests, each one born from a production bug. The office sees the job the same day instead of three days later.