Stub

2026  TestFlight beta

Native iOS · On-device reading · Money as read · Private matching

The whole cinema night in one app: what’s on, the friends you go with, the booking handed to the cinema’s own checkout, and the stub you keep afterwards. Aggregators own the moviegoer; Stub is built so every booking lands with the cinema, and so its own server learns as little as it can.

Role
Founder, designer & engineer
Stack
Swift · SwiftUI · Vision · Foundation Models · Metal · SwiftData · CloudKit · CryptoKit
Methods
A rule-based baseline the model is scored against, performance budgets in CI, a written decision for every product call
Scope
52,000 lines of Swift · 685 tests · 46 product decisions · 182 engineering records
Eight editions side by side, one in each of Stub’s eight movements
The night

The cinema should keep the customer

Most people find a film on an aggregator, book through it, and the cinema never learns who came. Stub covers the night end to end: what’s on near you, a plan your friends can answer, and then a hand-off to the cinema’s own checkout with the session already chosen. Booking seats inside Stub through the cinema’s Vista was built against the sandbox and then parked on purpose: the sale, and the risk that comes with it, stays with the cinema.

After the film, the ticket is read on the phone and kept, and each release prints its own edition — a ticket designed for that film, with your seat and your night on it.

We can’t create data. We can only have data we can credibly authenticate.
Money

An amount is shown only as it was read

An amount Stub shows, sums or sends is a claim, so every one carries where it came from. Money is held in integer minor units, never a float. Currency is an ISO 4217 code or nothing — no default, and nil is an answer. Each amount records whether it was printed, confirmed, typed or came from the cinema, and whether it is one seat, the whole booking or unknown. A session keeps the cinema’s own wall clock with no zone, because a 9 pm Saturday in Los Angeles is Sunday in UTC. Currencies are never added together. When only some nights have a price, the season says so: paid · 6 of 9.

Reading the ticket

A baseline the model has to beat

Vision reads the ticket on the device. A rule-based parser reads it first, always; the on-device model reads it with that draft in hand and is scored against it. On the eleven fixtures the rules get every field right in a millisecond, while the model takes a median of 11.7 seconds — so the rules’ draft goes on screen first, and shutter-to-keepable fell from 4.20 to 1.50 seconds. The honest gap is written down too: the corpus of real tickets is still empty, so there is no field rate yet worth quoting.

Friends

A match the server can’t read

When two friends both want the same film, both are told. Stub’s server never learns which film. Each phone keeps a private key in the Keychain; for each friend it agrees a secret only the two phones share (X25519, then HKDF), and sends the film as a token hashed under it. The server holds tokens it cannot read and only notices when two of them match.

The back of an edition: the scanned stub in photo corners, a pencil signature and the artist’s proof number The Brutalist in noir, punched twice for a third viewing
The back of an edition, signed; and a third viewing, punched.
Measured

Done means under budget on the oldest phone

Every budget is an Apple number or derived from one, on the oldest iPhone Stub supports: first frame in 400 ms, a tap answered in 100 ms, a ticket keepable 1.5 s after the shutter. UI tests measure launch, signposts, hitches, memory and CPU; a script fails any run that is over budget or stopped measuring something, on every push. A budget can be tightened in any commit; raising one takes a written reason.

685Tests
1.50 sShutter to a ticket you can keep
1 msMedian rule-based read
46Product decisions, written
Where it stands

In beta, not yet on the App Store

Build 1046 went to TestFlight on 4 November 2026. The source is private while it is in beta.