Build log

How Dibs got built.

A bill-splitting app, built solo in New York, in three bursts across five months.

Everything below comes out of the actual git history, decision log, and eval records. No rounded-up numbers, and the parts that went badly are here too — the week of native Swift that got deleted, the ten weeks where nothing happened at all, and the scheduled jobs that turned out to have never run.
144
days, first commit to public TestFlight
178
commits — 78 in April, zero in May and June
23
TestFlight builds to reach one stranger
$120
total spend, including the domain
Timeline

Five months, forty working days.

Apr 6, 11:34 PM
First commit, on a Monday night. No code: a vision doc, a CLAUDE.md, a gitignore.
Apr 7–8
PRD, decision log, style guide, task backlog. The repo had a voice-and-tone document before it had a single button.
Apr 8
In one day: Supabase schema, the themed claim web app, and the host app’s auth → onboarding → tab loop.
Apr 9–17
Receipt scanning: Apple Vision OCR native module, a regex parser, an on-device 3B LLM, and a 224-receipt eval harness to find out none of it was as good as it felt.
Apr 21
Killed the on-device LLM, five days after shipping it. Replaced with Gemini Flash behind an Edge Function.
Apr 21 → Jul 4
Nothing. Ten weeks of silence.
Jul 4–7
Came back. Rebuilt the eval on 118 hand-verified gold receipts, shipped two prompt overhauls (10× latency win), a self-learning loop, TestFlight build 10.
Jul 7 → Aug 11
Quiet again. Five weeks.
Aug 11–28
The launch grind: security hardening, two adversarial “no-go” reviews and the fixes they demanded, ops alerting, branded auth emails, a feedback→Linear pipeline, builds 11 through 23.
Aug 27
Build 23 submitted for the first-ever external Beta App Review.
Aug 29
TestFlight public link goes live. First commit → strangers can install: 144 days, of which roughly 40 had any commits at all.
The stack

What it’s made of, and why.

  • Expo + React Native, custom dev client, iOS only. Needed native access for Apple Vision OCR and the iOS Messages composer. Expo Go can’t do either; bare React Native is too much to maintain solo.
  • Supabase for everything — Postgres, Auth, Edge Functions, Realtime, Storage. One vendor, open-source primitives, free tier covers v1. Every money-state transition lives in an Edge Function; the client is never trusted.
  • Next.js on Vercel for the guest side. Friends claim from a link in their texts — no app, no account, no cookie banner. The iMessage link preview is generated per tab and treated as first-class design, because it’s the first thing every guest ever sees of Dibs.
  • No SMS provider. Invites go out through the host’s own Messages app, prefilled. A provider would have cost roughly $0.10 per tab and one to two weeks of carrier paperwork; the host’s own number is free and — more importantly — the text comes from a friend, not a shortcode.
  • No payment processor. Venmo, Cash App, and PayPal.me deep links, with the host confirming receipt by hand. Dibs never touches the money, which is exactly why it can be free.
  • Apple Vision OCR on-device, Gemini 2.5 Flash for structure. The photo never has to leave the phone for text extraction; only the extracted lines go to the parser. About $0.001 per receipt.
Problem one

Reading receipts ate the most nights.

Receipt parsing went through three complete architectures. The only reason it converged is that an eval harness replaced vibes with numbers.

Round 1: regex

Handles clean Toast and Square receipts, falls apart on everything else. Across 224 real US receipt photos it found the right total 59.8% of the time and the right item names 18.2% of the time.

Round 2: an on-device 3B model

Apple Foundation Models on iOS 26. A beautiful idea — free, offline, guided generation straight into typed Swift structs. Measured reality: it hallucinated tips so badly that tip accuracy came in at 4.9%. It read the line “Separate checks: 6-of-6” as a menu item, and then as six cups of chowder. A week of Swift work, 191 lines of native module, deleted five days after it shipped. The decision-log entry that killed it is dated four days after the entry that introduced it.

Round 3: Gemini 2.5 Flash behind an Edge Function

This one was earned with an eval rather than chosen on faith: 118 image-verified gold receipts with arithmetic-gated labels. Gemini beat regex on every single field, including tip at 99.1%. Then prompt engineering against a scoreboard — v2 (structured output plus few-shot) took items-F1 from 91.9 to 94.1 and latency from about 15 seconds to 1.3. v3 and v4 measured worse and were rejected. v5 shipped merchant-name repair while re-anchoring the money rule, every dollar value stays exactly as printed, digit for digit, because v4 had proved that letting the model fix names made it start fixing totals too.

  • Tip parsed correctly4.9%99.2%
  • Receipt math reconciles to the penny21%75%
  • Parse latency~15s1.3s

The regex parser survived, demoted but load-bearing: it paints the review screen instantly while Gemini runs, and its agreement with Gemini is how the app computes a real per-field confidence score instead of a hardcoded 0.95.

The harness and the parser are open source. The eval that produced every number in this section — and the regex parser it measured against — are on GitHub. It runs on public receipt datasets, so the regex column reproduces. The Gemini prompts are held back; what each version changed, and what it cost, is written up there anyway.
Favorite failure: one receipt’s merchant line OCR’d as “Place Your Card Here.” The model looked straight past it and answered “COMMANDER’S PALACE.” The regex parser proudly reported that the restaurant was named Place Your Card Here.
Problem two

Guest identity, without accounts.

The whole product bet is that your friends never sign up. But one shared link per tab means anyone holding the link could claim to be anyone — and an adversarial review rated that a launch blocker. Mischief, with money attached, on the core flow.

The fix that preserved zero-signup is first bind wins. The first browser to say “I’m Priya” gets a 32-byte secret minted server-side and stored in an httpOnly cookie; every mutation after that requires a timing-safe match. The second person to claim to be Priya is told Priya is taken. No account, no verification step, no visible change to the guest experience — identity theft within a tab just stops working.

Problem three

The ops jobs that had never run.

Late August, during the go/no-go review, a live check of production found that all four scheduled jobs — daily ops report, usage alerts, receipt-image cleanup, learning digest — had never fired. Not once, since the day they were scheduled. A database setting they depended on had never been set in production, and Postgres’s cron bookkeeping reports “succeeded” when an HTTP job has merely been queued. Everything looked green for months.

The alarming part: the cleanup job not running meant April’s receipt images were still sitting in storage, which looked like a breach of the privacy policy’s seven-day deletion promise. The audit found five objects, all belonging to still-open test tabs, and zero actual violations. The relief was disproportionate to the object count.

The rebuild moved secrets into Supabase Vault, re-scheduled every job to fail loudly with named errors, and keyed alerting off actual HTTP responses instead of the job table that had been lying. Plus a runbook, because a watchdog can’t alert you when the watchdog is the thing that’s down. On August 26, the daily ops report sent for the first time ever.

Numbers

What five months actually cost.

  • 178 commits over 144 days — 78 in April, zero in May, zero in June, 36 in July, 64 in August.
  • ~36,300 lines of TypeScript, SQL, Swift, and Python, plus ~5,700 lines of Markdown. The docs-to-code ratio is not an accident — the planning docs are the reason the two long breaks didn’t kill the project.
  • 46 database migrations and 29 Edge Functions.
  • 18 commits between 11 PM and 4 AM, including the very first one.
  • 23 TestFlight builds to get one into a stranger’s hands.
  • Total spend: $99 for the Apple Developer Program, ~$20 for the domain, and ~$1.20 of a self-imposed $100 Gemini eval budget. Everything else — Vercel, Supabase, Resend, PostHog, Sentry, EAS — on free tiers. Marginal cost per receipt parsed: about a tenth of a cent.
Close calls

Moments that almost killed it.

  1. The ten-week disappearance. April ended with the app “almost ready for the App Store.” Then nothing until July 4th. Every side project knows this cliff. What pulled it back wasn’t discipline — it was that the decision log and task backlog made re-entry cheap. Day one back was shipping, not archaeology.
  2. The on-device LLM dead end. A week of native Swift — guided generation, prewarming, session management — measurably worse than a cloud call costing a tenth of a cent. Deleting it stung. The eval made the decision impersonal, which is the only reason it took five days instead of five weeks.
  3. Two adversarial “no-go” verdicts in the same week of late August: one for the guest-identity hole, one for a leaked phone-number field and a broken onboarding path. Both reviews were run on purpose, and both were right. Launch moved a week; the alternative was launching with holes.
  4. The dead CTA. The website’s “get the beta” button had pointed at the TestFlight public link since July. Discovered in late August: that link had been dormant the entire time, because external TestFlight links don’t go live until a first Beta App Review is requested and approved, which had never happened. Builds 11 through 22 were reaching an external audience of zero. Build 23 went through review, the link lit up on August 29, and the funnel existed for the first time.
Footnotes

Surprising, funny, all true.

  • The first production login-support incident was a user who couldn’t sign in because he’d signed up with Google and was trying email and password. The account was perfectly healthy. The same debugging session established that Supabase’s admin list endpoint returns empty identities as an artifact — so the investigation initially “confirmed” a corrupted account that didn’t exist.
  • A config footgun, recorded for posterity: setting a nonzero thinking budget on the feedback classifier silently ate the output token budget and made the model return nothing at all. The fix was telling the model to think less.
  • The privacy policy briefly claimed the receipt photo “never leaves your phone.” True of the OCR — but the app also uploads the image so the host can re-check it later. Caught in an audit and corrected. Honesty is cheaper before launch than after.
  • The invite your friends receive is sent from the host’s own phone number, which means Dibs ships an entire messaging feature consisting, in total, of prefilling a text box.
  • Tip accuracy of the fancy on-device model: 4.9%. Tip accuracy of a regular expression written in an afternoon: 91.1%. Machine learning is humbling in both directions.
All of that, so dinner takes one minute.

Join the beta · See how it works · questions to hello@getdibs.app