Case study · Field Ledger
A ledger small enough to trust.
A tool and asset tracking PWA for small-to-mid-sized construction contractors. Append-only ledger of tool movements. First pilot live with Crizer Construction. The scope discipline is the product.
The job
Small-to-mid-sized contractors lose time, money, and trust when shared tools are hard to locate. Drills, generators, lasers, jackhammers ride from yard to truck to jobsite to a sub and back. Without a system, finding them is a phone tree, and replacing the ones that vanish is a tax on every project.
Field Ledger keeps an append-only ledger of tool movements so owners can see where assets are and field workers can check tools in and out from a phone. Tagline taken straight from the login page: “Know where every tool is. Always.”
Constraints
The site is the jobsite. The user is in gloves. The connection drops. Every design choice answered to that. No passwords — magic link only. Scanning a sun-bleached QR sticker had to work at an angle, in a glove, with two bars of signal. If the network was gone, the scan still had to happen.
The append-only property was the other anchor. Tool movements are facts, not opinions. If a tool was marked missing by mistake, the correction is a new “found” event, not an edit to the old row. That property is what makes the ledger usable as evidence in a dispute about who had what when.
What I built
Next.js 16 App Router, React 19, TypeScript. Supabase for Postgres, Auth, and Row-Level Security. Two ledger tables anchor the schema: `tool_events` for operational state (seven event types in a Postgres enum — checkout to job, check-in to shop, mark missing, found, move to service, return from service, report damage) and `ledger_events` as the analytics stream for cross-cutting reporting.
Append-only is enforced at the database, not the application. RLS policies block UPDATE and DELETE on `tool_events` for every role. Even admins cannot edit history. Current state derives from triggers that project the latest event onto a denormalized `tools.status`, `tools.current_location`, and `tools.current_job_id` after each insert.
Three roles, scoped per company: admin, foreman, field. Field workers can scan and log normal movements. Foremen can create jobs and tools. Admins can manage memberships and invites. Role checks live in both the database (Postgres helper functions like `is_admin(company_id)`) and the UI permission helpers, so a misbehaving client can’t escalate.
Offline-first via IndexedDB. Scans queue into a `tool_events_queue` object store with a `synced` flag. When the device reconnects, a flush replays the queue against the server. The UI subscribes to a `field-ledger-queue-changed` custom event so the pending counter stays accurate.
QR labels generate as a 4×5 grid of 2-inch squares on A4 — twenty labels per sheet — via jsPDF with error-correction level H, so a peeled corner doesn’t kill the scan. The payload is a short URL: `/{companySlug}/t/{toolCode}`. Scanning uses `html5-qrcode` on iOS and the native `BarcodeDetector` API on desktop where available, with feature detection picking the better path.
What I deliberately did not build
These are listed as out of scope in the README. They were not deferred for capacity reasons. Each would have made the ledger less useful as a tool-tracking system by widening what an “event” means.
- No maintenance schedules — Field Ledger tracks where a tool is, not whether it’s due for service.
- No fuel logs — that belongs to the truck, not the tool.
- No inspections — the compliance layer would have changed the meaning of every event in the ledger.
- No GPS or telematics — the ledger is a record of human actions, not a tracker.
- No registration or insurance ingestion — those live in the contractor’s office, not the field.
What the team does without me
Crizer Construction is the first pilot, live on the production domain at buildwithledger.com. A runbook in the repo documents what Sam (Crizer admin) needed to do to onboard, what the pilot agreement covers, and how the company was seeded.
The team scans, logs, and reviews on their own. New tools and jobs are created in-app — admins do not need to ask me. When the pilot expands, additional companies get seeded with a slug and an admin invite. No new code per pilot.
A CLAUDE.md and a README document the architecture, the out-of-scope list, and the conventions. A future engineer — or me, six months from now — can extend the ledger without re-deriving the constraints.
Tools come back. The crew sees them coming back. That is the whole product.