Contact

rowntree.dev

I build on the seam between the digital and the physical. A code on a watch that a shop scanner will read. A message you can only read by standing where it was left. An AI that runs on a box in a place with no signal, answering out of documents you control.

That seam is where the interesting problems are. Software that stays inside a screen can assume almost anything; software that has to meet the world gets no such comfort. It has to survive a cheap laser scanner, a 96 KB device, bad light, a wrong guess about where someone is standing. Some of these are tools I use every day. Some are arguments I wanted to make in public. I built and shipped all of them end to end.

Work

A Garmin fēnix 8 watch showing a card named Gym as a full-screen QR code, rendered by Keychain.

01

Keychain

Your loyalty cards, on your wrist — in a form a shop scanner accepts.

Every gym tag, grocery card, library number, transit pass and parkrun ID lives in a wallet you left at home or a phone you have to dig out. Keychain puts them on the watch you are already wearing: open a card and it fills the screen at boosted brightness, ready for the scanner. The code is generated on the device: no phone, no signal, no account, nothing uploaded. The free app holds three cards; Keychain Pro holds twenty, with on-watch reordering.

The hard part. One source tree, 138 devices: round, semi-round and rectangular; AMOLED, MIP and LCD; a 96 KB memory floor at the bottom of the range. Connect IQ has no code-generation library, so all ten symbologies are hand-written in Monkey C: QR (with a capacity ladder that falls back through error-correction levels before it refuses), Aztec, PDF417, DataMatrix and five 1D formats. Each is pinned to byte-exact golden matrices verified against zbar, ZXing and dmtxwrite. Layout derives from screen geometry rather than device lists, a generator emits four manifests and seven build configs from one device table behind a CI drift gate, and a compile matrix builds every device × tier before anything ships.

What it taught me. Constraint is a product decision, not an engineering one. The 96 KB devices could not hold every encoder, so the two densest formats compile out on exactly those five models and say so plainly instead of failing at the till; the paid tier is a build-time constant rather than a runtime flag, which removed a whole class of entitlement bug before it could exist.

Platform
Garmin Connect IQ · Monkey C
Reach
138 devices · 33 languages · 26 translated store listings
Model
Free tier + paid Pro tier via Garmin Pay

1,000+ downloads · 4.6 from 10 reviews · v2.1.2 as of 27 July 2026

Available on the Connect IQ Store
The Keychain Scanner web app: a headline reading Read your card's value, a list of the ten supported code formats, and buttons to scan with the camera or choose a photo.

02

Keychain Scanner

Point a camera at a plastic card and get the number back. Nothing leaves the phone.

The slowest step in setting up Keychain was copying a thirteen-digit number off the back of a card by eye. The scanner removes it: point the camera, or drop in a photo, and it hands you the value to paste, along with a preview of how the watch will draw it and a verdict on whether it will scan at that size. It is the link the Connect IQ listing sends people to in English and all 26 translated listings, replacing the third-party scanners that listing used to point at.

The hard part. “Nothing is uploaded” is a claim most sites make and none prove, so here it is enforced rather than promised: the WebAssembly decoder is inlined into the bundle so it never fetches, fonts are self-hosted, icons are inline SVG, and the CSP sets connect-src 'none'. The page has no way to make a network request, even if a dependency were compromised tomorrow. Real-world photos are the other half: a barcode usually sits in a band of a larger frame and fails to binarize when a dark background bleeds in, so the decoder sweeps the whole frame, a centre square, then inset bands, across two binarizer passes, and takes the first hit.

What I was testing. Whether a companion app can be a feature of the parent rather than a separate product. The watch's own encoders are ported 1:1 to TypeScript and golden-tested against the Monkey C matrices, so the preview is the exact symbol the watch will draw, not a generic library's approximation, and the same 33 languages ship on both sides.

Stack
Vite · vanilla TypeScript · zxing-wasm · Cloudflare Pages
Reads
QR, Aztec, DataMatrix, PDF417, Code 128, EAN-13/8, UPC-A, Code 39, Codabar
Privacy
Zero third-party requests · no trackers · works offline
The scrawl.place landing page: the lines you are here, so was someone else, they left something behind, now it's your turn, above three rules and a find me button.

03

scrawl.place

A message you can only read by standing where it was left.

Leave a note at a physical location. It can be read only by someone who comes to the same spot (within roughly 150 metres) and it stays there permanently, accumulating over time like real graffiti. There is no feed, no account and no way in from your sofa. A toy, not a product: an argument that being somewhere is a scarce input worth designing around.

The hard part. Keeping it honest at zero marginal cost. No framework and no build step: plain HTML, CSS and JavaScript on Cloudflare Pages, with Pages Functions over D1 for storage and KV for rate limiting. A location resolves to a geohash tile before it is ever written down, so the database holds “this square” and never “this person, here”. A scheduled Action snapshots the database to R2 weekly, because permanent is a promise you have to actually keep.

Stack
Cloudflare Pages Functions · D1 · KV · no build step
Privacy
Geohash tiles, never raw coordinates · anonymous · no account

About an app, a bug, or work. It reaches me directly; your address is used only to reply, and is not stored or passed on.