Case study · Live on the App Store

Flying Start

A complete sailing race platform — sailor, race officer, club and backend — built solo in four and a half months.

Commits
974
Elapsed
4.5 months
Application code
~161,000
Shipped surfaces
7
Languages
22
Test cases
~377

The problem

Sailing has two entirely separate software problems and, until now, no joined-up tool for either.

The sailor needs to cross the start line at full speed at the exact instant of the gun — and not a second before, because being over early is a disqualifiable offence. Doing this properly has meant a dedicated GPS instrument bolted to the boat, costing several hundred to well over a thousand pounds.

The race officer is one person on a committee boat firing a start sequence, watching for boats over the line, capturing finish times for forty boats crossing in eight seconds, applying penalties, and then scoring a series under the Racing Rules of Sailing. Historically: a clipboard, a stopwatch, a horn, and an evening of data entry afterwards.

Why it is harder than it looks

  • Timing has legal weight. A finish time is a competitive fact that gets formally protested. It can never be lost, and it must survive the device dying mid-race.
  • The environment is the worst case for software. A boat, on water, no cell signal, spray on the screen, gloved hands, one operator, and no second chance at the moment that matters.
  • Scoring is a real rules engine. Six handicap systems, two points systems, fifteen penalty codes, penalties that can never be discarded, and a tie-break cascade defined in an appendix of the rules.
  • Everything must agree. Two apps across two platforms plus a web console, sometimes with no network between them, all holding the same race state.

What was built

SurfacePlatform
Sailor app — start timer, distance and time to line, over-early alarmiPhone, live on the App Store
Race Officer app — start sequences, finish capture, penalties, scoringiPhone & iPad, live on the App Store
Watch app — countdown, haptic start sequence, standalone GPS modeApple Watch, with complications
Widgets and Live Activities for both appsiOS
Sailor and Race Officer appsAndroid
Race officer web consoleWeb
Marketing site, community site, internal ops consoleWeb
Postgres backend, realtime sync, serverless functions, Stripe ConnectCloud

The parts I would show an engineer

Offline-first sync that knows when to be clever and when not to be

Every change writes to the device first and queues for the server. Deletes go through a durable tombstone queue so a delete that happened offline is not resurrected by a stale sync.

The conflict strategy is deliberately different per kind of data, which is the part I care about. Timing facts — laps, finishes, results — use an idempotent upsert on a natural key with last-write-wins, because two officers recording the same finish are recording the same physical event. Shared reference data uses true optimistic locking with a version check, and surfaces the conflict to the officer, because two officers editing a competitor are recording different facts and last-write-wins would silently eat one of them.

Invariants no client can be trusted to hold live in the database as constraints and triggers — including a renumbering trigger that keeps races in chronological order under concurrent inserts and deletes without ever violating its own unique constraint.

The design document for this is written as a defence, with a section headed what we deliberately did not build and a register of known weaknesses, each with a severity and an upgrade path. It names the things that are still wrong. That is the document I would want to read before hiring someone.

Three different cross-device problems, three different answers

  • Watch to phone: a compact state snapshot streamed several times a second. If the phone disappears, the watch rebuilds the race from its cached snapshot and carries on standalone from its own GPS.
  • Phone to phone: a peer-to-peer mesh over local wireless so every crew member's wrist gets the same haptic countdown, with no infrastructure at all.
  • Device to cloud: realtime subscriptions with a polling fallback, both draining through the same merge path so there is only one place where merging can be wrong.

An on-device machine learning model

Predicting time-to-line from GPS alone is noisy. A small residual neural network, trained in PyTorch and exported to run on the phone, learns the error in the filtered estimate and corrects it — using speed, heading, acceleration, distance to line and GPS accuracy over a rolling window. Training, evaluation and export are a scripted pipeline, not a one-off notebook.

Talking to real boat hardware

A full protocol stack for marine instruments: NMEA 0183, NMEA 2000, SignalK and Bluetooth wind sensors, over Bluetooth, TCP, UDP and WebSocket, with device discovery. Every parser and transport has tests behind a mock, because you cannot unit-test against a boat.

Twenty-two languages, enforced by CI

1,720 localised strings across the apps, and roughly forty locales on the marketing site. Translation drift is not a code review problem — three linters gate every push, so a new string cannot ship untranslated. The App Store listing document classifies locales into quality tiers and flags that specialist sailing terminology in the weakest tier needs a native-speaking sailor to review it before it goes near a store page.

Payments, properly

Stripe Connect in production, so clubs onboard and take their own entry fees, with refunds and payouts handled by the platform. Subscriptions on both the App Store and Google Play.

How it is verified

Roughly 377 test cases across the shared Swift packages, plus Android tests and end-to-end browser tests on the web console. Then the part that actually matters: the scoring engine was run against 8,424 real published races and diffed line by line against the incumbent application — 144,539 comparisons, 99% exact match, zero engine bugs.

How the verification works →

Things that went wrong

Two App Store rejections on the first release — one over price references inside screenshots, one over missing subscription links in the metadata. Both are now rules written into the release document so they cannot happen twice.

A hardcoded API key made it into the codebase. A security audit caught it; the key was rotated, moved into a secret, and the traffic re-routed through an authenticated proxy. It is written up, along with the standing rules that came out of it.

There is a post-mortem on the peer-to-peer sync in the repository. I would rather show you that than a list of things that went well.

The point

This was one person, four and a half months, alongside a full-time job. Not because of heroics — because the delivery apparatus around the code was built first, and the agent was pointed at a problem that had already been properly specified.

That apparatus is transferable. It is what I bring to a client.