The best things arrive late and fit perfectly. Mobile is on its way.

The desktop site is ready when you are.

Shon J.

Shon J.

Case Study

Garage

Everything your motorcycle has been through, in one place. Fills, services, documents and every rupee spent, turned into economy trends, per-component health and reminders that arrive before the damage does.

Role
Built end to end, alone
Years
2026
Status
Live
Built with
Claude Code · React · TypeScript · Vite · Cloudflare Workers · D1 · R2 · three.js

02The One Idea

A motorcycle generates a stream of small facts. Fuel goes in, kilometres go by, a chain gets lubed, a service happens, insurance lapses. Every one of those facts is trivial on its own and worthless in isolation.

The 3D bike, with health hotspots Image to come
The 3D bike, with health hotspots

03The Outcome

  • 29,500Lines of TypeScriptexcluding tests
  • 366Tests, in 32 filesabout 3,650 lines
  • 21Database tablesacross 17 migrations
  • 99Catalogued models21 makes, 61 variants

04The Proof

Five digits with no context around them, which makes the odometer the easiest thing in the app to misread and the most expensive to get wrong: a bad reading corrupts every distance and economy figure derived after it. Four independent checks stand between a photograph and the log. Pick a way the read can go wrong and watch which one catches it.

The Photograph

Both renderings agree, and the reading sits where a fortnight of riding would put it.

Last Entry
17,412 km
Days Since
14
This Rider Covers
38 km a day
So the Dial Should Read
~17,944 km
Rendering One17,887
Rendering Two17,887

  1. 01

    Two Renderings

    Pass

    The same photograph is read at two sizes concurrently. The leading 1 of a seven-segment display is a single thin bar with no enclosed shape, and resampling loses it. Two readings that disagree are evidence that one is wrong.

  2. 02

    The Odometer Only Goes Up

    Pass

    A reading below the last entry is wrong by definition, and is almost always the trip meter.

  3. 03

    Jump Size

    Pass

    More than 20,000 kilometres on from the last entry is more likely a misread digit than a ride.

  4. 04

    The Projection

    Pass

    Built from how much this rider actually rides. A reading at less than half of where the dial should be by now means either a missing digit or a bike that has been in a shed. Both are worth asking about.

OutcomeFilled in

A confident reading is filled in. Anything doubtful is offered with the reason in plain language, because silently entering a wrong odometer is worse than asking.

05The Work

The 3D bike, with health hotspots Image to come
The 3D bike, with health hotspots

06In Depth

The rest of it, if you want it. Nothing above depends on opening any of these.

The BriefA motorcycle makes a stream of small facts, each worthless alone and none of them worth the friction of writing down.

A motorcycle generates a stream of small facts. Fuel goes in, kilometres go by, a chain gets lubed, a service happens, insurance lapses. Every one of those facts is trivial on its own and worthless in isolation, which is why almost nobody keeps them. Together they answer questions an owner actually has, and the friction of capturing them is the whole problem: the moments worth logging happen at a pump or a service centre, which is exactly where phone signal is worst.

The MoveStore what was observed, compute the rest, and mark on the screen which is which.

A motorcycle generates a stream of small facts. Fuel goes in, kilometres go by, a chain gets lubed, a service happens, insurance lapses. Every one of those facts is trivial on its own and worthless in isolation.

Together they answer questions an owner actually has. Garage collects those facts with as little friction as possible, and then does the arithmetic that turns them into answers.

The QuestionWhat Answers It
Is my bike drinking more fuel than it used to?Economy by the full-tank method, and a ranked list of what to check first
What does this machine cost me per kilometre, honestly?Cost per kilometre across fuel, service, care and every other expense
Which part is closest to needing attention right now?Twenty checkpoints and seven rituals, rolled up to eight hotspots on the bike
When does my insurance expire, and will I find out in time?Staged warnings at 30 days, 7 days and 1 day
How far did that ride actually go?The odometer where both ends were read, route and waypoints where they were not
The SpineFuel, distance, care, service and paperwork, and the arithmetic that turns them into answers.

Logging

Every form is a bottom sheet, so logging never loses the context underneath.

  • Quick log. One floating action, direct routes to fuel, odometer, chain, tyres, service, expense, care and trips.
  • Scan a fuel bill. Station, brand, grade, ethanol blend, litres, rate, total, invoice number and date.
  • Scan the odometer. The reading is extracted and the image is never stored.
  • Log by sentence. "Filled 500 at HP on the way to work" opens a draft with the fields populated.
  • Offline queue in IndexedDB, replayed in order. Anything the server actively rejected is not queued.
  • Station recognition from your own history, not a map service. Silent within 10 metres, one tap out to 250.

The Insights Engine

Pure, synchronous, no React and no network. The same code runs online, offline and in the tests.

  • Economy by the full-tank method. Partial fills accumulate; a missed fill voids the segment rather than inflating it.
  • Provisional economy from the catalogue, labelled claimed rather than measured, with a count of tanks to go.
  • Economy diagnosis. Below the baseline by 8 percent, a ranked list: free things, then consumables, then real work.
  • Component health scored on the fraction of each interval consumed, rolled up to eight hotspots.
  • Inspections reset when marked OK. Replacements do not, because brake pads wear whether or not somebody looked.
  • Condition as a verdict. The score exists and is never shown, only its band, with what is dragging it down.
  • Riding rate over a 90-day window that widens only when the narrow one has too little to say.
  • Fill rhythm learned from your own history, in both days and kilometres, whichever comes first.
  • One ask at a time. A single ranked prompt for the whole app, each carrying its effort: one tap, a photo, or a minute.
  • Cost analysis, anomaly detection, year in review, and one ranked queue for maintenance and paperwork together.

Trips

  • A layer over the ordinary log, not a copy. Deleting a trip removes the grouping and keeps every entry.
  • Distance from the odometer where both ends were read.
  • Where the closing reading was never entered, location fixes give a floor and the fills become waypoints.
  • Every figure carries its source: odometer, route, waypoints or straight line, in descending order of trust.

Paperwork

  • Insurance, PUC, registration, warranty and licence, each with its own renewal period.
  • Expiry warnings staged at 30 days, 7 days and 1 day, with silence in between.
  • The one thing the app will send email about.

Bikes and Instruments

  • Any number of bikes per account, archivable and restorable with their full history.
  • A stylised motorcycle assembled procedurally from three.js primitives. No asset to download, no licence to manage.
  • Eight health pins anchored to the model's own coordinate space, so the engine pin sits on the engine.
  • A cluster replacement is stored as its own event, splitting the bike's life into eras. Readings are never rewritten.
  • Lifetime distance is derived at the one point raw data enters the engine, so everything downstream reads as one instrument.

Your Data, Back

  • Google Drive backup, the whole account as one file in your own Drive. Manual button and nightly sweep, one code path.
  • The Drive refresh token is AES-GCM encrypted at rest, so a copy of the database is useless on its own.
  • Export and import in the same portable format.
  • Deleting a bike writes and records the export first, and delivery is a separate retrying job.
  • An account can be closed from inside the app, with history handed back on the way out.
Where AI Is Allowed to ActThree engines read the photograph. None of them is believed without four checks agreeing first.

Photographs are read by a three-stage chain, all of it behind the session cookie. Every stage passes through the same trust boundary afterwards, so the parser does not care which engine spoke.

WhereWhat it doesThe gate
Stage one, Cloud VisionDense-document text detection returns flat text and word boxesWord boxes are what make an odometer readable at all: the total is marked out only by sitting beside the ODO caption
Stage two, Gemini 3 FlashMultimodal, temperature zero, strict output schemaStarted alongside Vision on a 1.2 second hedge. A healthy scan finishes first and never starts it
Stage three, Workers AILlama 4 Scout and Mistral Small, free, tried lastSame trust boundary as the two above it
Reading a fuel billPrinted field names at the start of a line become fieldsA normaliser reconciles litres, rate and total against each other
Implausible figuresAnything outside what a motorcycle can physically doDropped rather than written
InferenceAnything inferred rather than read off the pageFlagged in the interface, beside the figure
Reading an odometerThe same photograph read at two sizes concurrentlyFour independent checks, then filled in or offered with the reason
Station lookupAn unfamiliar pump is matched on OpenStreetMapThrough the Worker, so it carries the service identity and not your IP, with coordinates rounded to about eleven metres first
Under the HoodThe insights engine is a pure module with no React, no network and no clock, so every claim about a bike can be called from a test.

An edge-first build with no origin server: the API, the static assets, the database, the object store, the image models and the cron triggers all run on Cloudflare. The insights engine is a pure, synchronous TypeScript module with no React and no network, so the same code runs on fresh data, on cached data while offline, and inside the test suite against fixtures with known answers. Zod schemas shared by the browser and the Worker mean a field cannot be valid on one side and invalid on the other. Money is stored in paise and distance in whole kilometres, so no floating point goes near the arithmetic. The three-stage reading chain is tested against real API responses recorded once and committed, keyed by the hash of the image, so the parsers and the trust boundaries run against genuine model output offline and free on every test run.

worker/Hono API on Cloudflare Workers, Drizzle over D1, R2 for receipts
shared/Zod schemas and types used by both sides. One definition of valid
src/lib/The insights engine: pure, synchronous, no React, no network
src/The app itself

The insights engine is a pure module, and that is what makes it testable

No React, no network, no clock of its own. The same code runs on fresh data, on cached data while offline, and inside the test suite against fixtures with known answers. Everything the app claims about a bike comes out of one function that can be called from a test file.

One definition of valid

Zod schemas in the shared folder are imported by both the browser and the Worker, so a field cannot be valid on one side and invalid on the other.

Integers, plain dates, and sortable keys

Money in paise, distance in whole kilometres, volume in millilitres, so no floating point goes anywhere near the arithmetic. Log dates are YYYY-MM-DD and are treated as UTC noon for day arithmetic, so a fill logged at 11pm does not become yesterday in a chart. UUIDv7 throughout, so primary keys sort by creation time.

Edge first, and one request per screen

The API, the assets, the database, the object store, the image models and the cron triggers all run on Cloudflare. There is no origin server. A bike bundle endpoint returns everything the engine needs for one motorcycle in a single response, cached and recomputed client-side.

The CraftEvery derived figure carries a tilde and a reason. No evidence yet shows as a dash, never as zero.

Approximation has a convention

Every figure the app derived rather than observed carries a tilde, so ~396 km reads as approximate at a glance, in a table, on a chart axis, with no legend. Each one also carries a plain-language reason for the caption beside it.

Unknown is not zero

No distance evidence yet shows as a dash, never as 0 km. Nothing spent yet is never a cost of zero per kilometre. A number the app has not earned is worse than no number, because the reader cannot tell which one they are looking at.

Two themes on two axes

A default theme built around a warm ember accent, and Void, built around indigo. Each renders in dark or light, with system following the device. The two axes are independent attributes on the document root, so the stylesheet can express "Void, in light" as a plain two-attribute selector.

Contrast is tested, not assumed

A test reads the token stylesheet itself and checks every theme and mode against WCAG AA. It found real failures the day it was written: fifty-odd pairs below 4.5:1 in the light theme, and two below 3.0.

Sheets, not pages

Every form is a bottom sheet over the screen that opened it, so logging never loses context. The whole app is meant to be used one-handed at a fuel pump.

Motion with respect, and empty states that were drawn

Animated sheets, rolling numbers, count-ups and a scanning shimmer over a receipt thumbnail, all of which stand down when the operating system asks for reduced motion. Every screen that can be empty has a custom illustration rather than a grey box.

What is guardedHow
The insights engineFixtures with known answers, covering economy, health, costs and projections
The reading chainRecorded real-world API output, keyed by the hash of the image
Colour contrastA test reads the token stylesheet and checks every theme and mode against WCAG AA
MigrationsA guard script refuses to start the local Worker when migrations have not been applied
TypesThe build runs a full TypeScript check before it bundles
ScansStructured logging with the parser's output and the model's token usage, so an unmet bill format arrives as evidence

This page follows the convention it describes. The one figure on it that was derived rather than observed, the reading the dial should show by now, carries its tilde.

Store what was observed, compute the rest, and mark what was computed.

07Next