Skip to content
7 min read

GolfRay: a caddie that ships with no map

On this page

The number a golfer needs is not the number on the sprinkler head. It is what the shot plays like from where you are actually standing, into this wind, up this hill, with the club you actually hit rather than the one you remember hitting once. GolfRay is my attempt at that: a phone-first web app you install once and walk the course with. It reads GPS, works out which hole you are on, gives front, centre, and back yardages off the real green outline, and tells you what the shot plays like and which club covers it.

It is live now, and the thing I want to write about is the decision that shaped the release: the version you can open today makes no map-tile requests at all.

Fifty-nine loops in a day

The build ran as a loop I kept a log of: verify health, advance one increment, write it down. Fifty-nine iterations on the sixth of July, each one a green build plus the test suite plus a hands-on check in the preview, then exactly one new thing. Hazard carry distances. Hazard markers. A green-depth readout. Undo the last shot. A coaching tip synthesized from the round's stats.

The log is where the bugs live, and the first one set the tone. The seed data placed crowd notes with PostGIS's ST_Project, and I had the argument order wrong: azimuth where the distance goes. Every note landed about a metre from its anchor instead of out on the fairway, so the hole-four bunker note read "1 YD AWAY" while the carry alert for the same bunker said 250. Two numbers on one screen that could not both be true. That is what verification in the loop is for.

Iteration 58 was smaller and sneakier. Undo removed the shot with the latest timestamp, and two quick taps can land in the same millisecond, so the reduce tied on the first entry and deleted the wrong club. It removed the driver and kept the seven iron. The fix was to stop pretending timestamps are a total order and remove the last shot in the order the screen shows them.

A GPS fix is a claim, not a fact

Phones lie about where you are, confidently and in bursts. A wifi-triangulated fix with 500 metres of uncertainty will happily arrive between two good satellite fixes and yank the yardage across the fairway.

So the position the app displays is not the position the phone reports. It goes through a pure function, smoothFix, that is easy to unit test because it has no idea a phone exists. The first fix is always accepted, because a player under bad signal still deserves a number. After that, a fix worse than 50 metres that is also worse than twice the previous accuracy is dropped. A jump bigger than 25 metres with believable accuracy is treated as real movement and snapped to, otherwise walking to the next tee lags. Anything smaller is blended toward the raw fix, weighted by its accuracy.

The rule I added last is the one I would not have thought of at a desk: the gate releases after three consecutive rejections. Without it, one unusually sharp fix under tree cover freezes your position forever, because everything after it looks worse by comparison.

Your carries, not the tour's

The starting bag is a table of typical amateur carries. The interesting part is replacing it with yours, which the app does from logged shots: within one hole of one round, the distance a club moved the ball before the next shot.

Grouping by round is load-bearing, and there is a test named for the bug it prevents: never pair the last shot of hole nine in one round with the first shot of hole nine in the next round, or you fabricate a carry. Shots logged from a simulated tap on the map are skipped for the same reason. Per club, the app uses the median rather than the mean, so one thinned eight iron does not move the caddie, and it shows you the 25th-to-75th percentile spread instead of a single number, because amateurs remember their best strike as normal and under-club for the rest of their lives.

Plays-like is arithmetic

The plays-like distance is deterministic physics rules of thumb, the same first-order adjustments the commercial caddie apps reduce to, with no model in the middle. A headwind costs about 1% of carry per mile per hour of headwind component; a tailwind gives back half that, because wind hurts more than it helps. Elevation is a yard per yard. Temperature is about 1% per ten degrees from a 70°F baseline. Altitude is about 2% per thousand feet of thinner air. The wind term is capped at 30%, because a gale cannot make a wedge play like a driver, and the whole thing collapses to the raw yardage when conditions are null, which is what offline looks like. Weather comes from Open-Meteo, which needs no key.

The strokes-gained card uses Mark Broadie's published PGA Tour tables, typed in from the Columbia papers and cross-checked three ways, and it says out loud what it cannot know. GPS cannot read your lie, so approach shots use the fairway benchmark. The pin is not known, so distances run to the green centre. Putt distances from GPS are approximate. The breakdown of where you leak strokes is robust to all of that; the total is directional. I would rather show a card with its caveats printed on it than a precise-looking number that is not.

The map that is not there

The demo had satellite imagery from the start, and the design assumed it: an offline course pack meant caching those tiles for the holes you were about to play. That is the feature. Nobody has signal on the back nine.

The tile provider's terms did not allow it. The development tiles the map library falls back to are not licensed for the caching at all, so the build now refuses to ship them. The next plan was a same-origin proxy in front of a licensed provider, which would also have kept the key off the phone. On the eighth of September I read that provider's cloud terms properly: proxying needs a custom agreement, and server-side caching and bulk downloads are restricted. Holding a subscription does not make the proxy allowed.

So the first public release has no imagery. Instead of a satellite photo it draws the course from the data that is actually mine to redistribute: OpenStreetMap green outlines, tee-to-green direction lines, and approximate hazard markers, for four Ohio courses, 72 holes, all with mapped greens. The derived course data is published alongside the app under the ODbL with attribution. The map makes zero tile requests, which also means the offline story is finally simple: after one online load, the app shell and the courses are cached, and nothing on the course depends on a server.

The same release turns the backend off. Rounds and notes stay in the browser's storage. The Supabase side, with its row-level security, trust-weighted crowd notes, and a relevance ranking that the client ports to eight decimal places of parity with the server function, is real and tested, and it is not what you get when you open the link. I would rather say that than imply a Course Brain that a stranger cannot see.

What OpenStreetMap will and will not give you

Importing the courses was its own lesson in honest data. A sweep of the Cleveland and Akron area found 97 mapped courses. Twenty-three had a property outline and no holes. Eight more had a handful of holes mapped, including a real 18-hole club with exactly one, and the importer now rejects anything under nine, because listing a course a golfer can select and then find unusable is worse than not listing it. Hinckley Hills maps a five-hole short course as B1 through B5 beside its main 18; those are excluded rather than mistaken for holes. One course tags a hole par 7, which the schema rightly refuses, so an implausible par falls back to 4 instead of aborting the whole import.

Open it

The launch runbook still lists the acceptance test I owe it: a full eighteen with the installed app on a real phone, comparing yardages against the course. Until then, the claim is narrower and I am fine with that. Open it on your phone, allow location, pick your course, and see if the number matches the shot.

Open GolfRay on your phone →