Bets — The Vault
The ledger for the handshake bets a friend group never writes down.

Watch it run
02 TAKESEvery feature, step by step
04 FEATURES · 06 SCREENS · OPEN ONEWriting one down
02 STEPSThe whole app is one screen: a form pinned above the ledger it writes to. A bet gets recorded at the moment both sides agree, which is the only moment anyone is honest about the terms.
Settling a one-off
01 STEPA One-Off resolves exactly once. Both names are buttons, and whichever gets pressed is the winner written against the row.
The wager that never ends
02 STEPSThe other kind. An Ongoing bet never resolves; it keeps a running tally per player that either side can add to, which is what a season-long argument actually looks like.
The ledger
01 STEPOne shared table, newest first, holding both kinds at once. There is no filter and no archive, so settling and deleting are the only two ways a wager leaves the page — which makes delete the one control that can lose a record.
The trailer at the top is built motion rather than app footage, and says so; everything below it is the real thing running. Every bet in either is invented, and it has to be: the real table is a ledger of wagers between me and my friends, so every name, stake and tally in it is theirs, and none of them agreed to appear on a public portfolio. So the wagers here are made up and so are the people — the same invented cast the Locked In captures use, on the same reasoning. My own name is the one real thing left, and only because it is already the title of this site. Nothing came out of the real project and nothing went into it: the build under capture is a copy of the app made outside its repository and compiled with synthetic Supabase credentials, the harness replaces fetch before any application code runs so the app issues exactly the five queries it always does and gets invented rows back, and every request to a Supabase host is intercepted and counted at two layers — the page and the network — with the run reporting the count and failing if it is not zero. The state changes are real writes rather than staged screens: the tally really goes from 6 to 7, and the one-off really resolves. The delete shot is taken deliberately inside the five-second undo window, so the row is still in the table at the moment it is photographed — the harness asserts exactly that, and then asserts the row is gone once the window has closed, because a screenshot of a toast otherwise proves only that a toast rendered. Three things changed in the app itself while this sheet was being made, and all three are worth naming. Capturing it at phone width surfaced a react-native-web layout bug that put the second name field over the edge of the card, so the fix — a single minWidth — went into the real source before these were shot, rather than the screenshots being framed to hide it. The other two were gaps this page originally described as open: a form that refused a bet without saying why, and a delete with no confirmation and no way back. Both are now fixed rather than merely admitted, and these captures are of the fixed app. Beyond that nothing is retouched; every screen, control and state is the app exactly as it runs.
My friends and I bet on things constantly and then argue about who owes what. The Vault is the ledger: a wager is written down with its terms, both sides and the stake at the moment it is made, and it stays open until somebody settles it. The record exists before the argument does.
How it’s put together
Built with
- PERIOD
- February 2026
- ROLE
- Solo — design and build
- SOURCE
- View on GitHub ↗
The hard parts
06 NOTESTwo kinds of wager, and they behave differently. A One-Off resolves exactly once — you tap whoever won and it locks with the winner recorded against the bet. An Ongoing wager never resolves; it keeps a running tally per player that either side can add to, which is what a season-long argument actually looks like.
A bet is only valid if it is complete: description, both players and the stake are all required before it can be locked in, so the vague ones never make it into the ledger. That guard used to be one early return in addBet, which meant it failed silently — the button simply did nothing. The bet with a missing term is precisely the one that gets argued about later, so an unexplained refusal loses the record it was trying to protect. It now names the field and marks it, and the mark clears as you type into it. Fields are trimmed before being checked and before being stored, because a stake of a single space is the same vague bet wearing a disguise.
The ledger is shared rather than per-device — every wager lives in Supabase, so both sides see the same open bets and the same settled ones.
There are no accounts at all. No sign-in, no user table, and the players are just names typed into a field rather than users who exist somewhere. It was built for one group of friends and handed to them directly, so the app never had to answer “who are you” — everyone holding it sees the same single ledger. The Supabase client is configured with AsyncStorage for session persistence, which is dead configuration: nothing in the app ever signs anyone in.
One React Native codebase produces both the Android APK, built through EAS and sent round the group, and a web build through react-native-web.
Building the same source for the web is where the abstraction leaks, and it leaks in one identifiable place. react-native-web renders a TextInput as an <input>, which carries a min-content width Yoga does not, so the paired name fields refused to shrink and overflowed the card below about 700px — every phone. Native was never affected. One minWidth fixes it, but only after knowing which of the two layout engines you are actually arguing with.
Want the parts that aren’t on this page — the architecture arguments, the things that broke, a live walkthrough?