Inspiration

Every shared-expense app I tried makes one person the accountant. Someone exports a spreadsheet, someone else forgets to log the groceries, and month-end reconciliation becomes an argument about memory.

I shipped BooKoo in 2017 on Expo, Redux and Firebase Realtime Database; by its last release in 2020 it was on Expo 36. It worked until it didn't: the client loaded an entire ledger's items into Redux and recomputed every total on every change. A ledger with two years of history turned the app into a spinner. I stopped maintaining it in 2020.

BooKoo 2026 is a ground-up rewrite — new bundle id, new backend, none of the old code. The one thing I carried over is the lesson: performance is the acceptance criterion, not a polish pass.

What it does

A ledger several people write to at the same time — a team's running costs, a small club's dues, a trip with friends, a household. Entries carry amount, category, memo, receipt photo and location, and each entry has its own comment thread — so "what was this ₩12,500?" gets answered where the entry lives instead of in a chat app. Calendar, timeline and category statistics all read the same data. It is a shared record, not a settle-up tool: BooKoo does not split bills or move money between people. Eleven languages, app lock, push.

The monetization idea is one sentence: Free keeps a complete ledger; Pro removes ads and opens it to the group.

Free is not a crippled demo. Free keeps one ledger you own, with unlimited entries and full access to your own data, plus joining any number of Pro-funded ledgers — with restrained ads that never interrupt entry creation. A new ledger holds 2 members on Free and up to 5 with Pro. Pro also lets you create additional ledgers, and lets one payer fund everyone: invited members of a subscriber's ledger get the ad-free Pro experience without paying anything.

How I built it

Client — Expo SDK 57, React Native 0.86, TypeScript, expo-router with typed routes. Every unbounded list is a FlashList fed by a paginated Convex query. Nothing loads a whole ledger.

Backend — Convex. Every query is index-bounded, every list paginates, and every dashboard number comes from a rollup table maintained incrementally on write. No query handler ever walks every entry to produce a total. That single rule is the one the 2017 app violated.

Monetization — RevenueCat, server-authoritative. This was the interesting part. A ledger-scoped entitlement has to unlock premium for people who never bought anything, so the client cannot be the authority:

  1. The RevenueCat webhook is the source of truth. Events land in Convex and update a subscribers table.
  2. A server-side REST read of /v1/subscribers/{app_user_id} closes the webhook-latency window right after a purchase, and self-heals drift.
  3. react-native-purchases CustomerInfo is presentation only — it may open a screen, it may never authorize a write.

Every premium write is re-checked in Convex against server state that came from RevenueCat, never from the device. Screens name a capability; the mapping to offerings and products lives in one shared module and in the RevenueCat dashboard, so no screen hardcodes a price or a product id.

Challenges I ran into

Entitlement that belongs to a ledger, not a device. The subscriber is the ledger's owner; every ledger they own inherits the entitlement, and members who never bought anything get Pro on that ledger. RevenueCat's model is per-subscriber, but BooKoo's unit is a ledger — a team, a club, a trip. Deciding whether a seat may be added means asking who funds the ledger it is being added to, not who is holding the phone. Getting that derivation server-side — and keeping it correct when a subscription lapses, or when the owner deletes their account and ownership, and with it the funding, passes to another member — took more design than the purchase flow itself.

The webhook latency window. Between a successful purchase and the webhook arriving, the user has paid and the server does not know it yet. Making them wait is unacceptable UX; trusting the client is unacceptable security. The server-side REST refresh closes that gap without moving authority to the device.

Rewriting a product that has real users. 2017 user data is being migrated in, so every table that receives migrated rows carried legacyUid / legacyKey from its first schema version. Retrofitting those later would have been painful.

What I learned

"Don't trust the client" is easy to say and structurally demanding to implement. The temptation is to read CustomerInfo and gate on it — it is right there, it is accurate most of the time, and it is one line. Separating presentation from authorization, and making the server derive entitlement from RevenueCat's state rather than from the app's belief, is what makes a ledger-wide subscription safe to ship.

What's next

A handful of founding groups — a team, a club, a trip — running real money through it. A tier for larger groups is deliberately deferred until real Pro demand justifies it, rather than shipped on speculation.

A note on the 2017 app

BooKoo 2026 is a new store listing, dev.hyo.bookoo, whose first public release on both stores happened in 2026 — it had no earlier version on either store. A different app, com.dooboolab.bookoo, was published in 2017 by me under the same name and has since been discontinued and is no longer available on either store. The two share no source code, no backend, no authentication system and no data model. I am stating this openly rather than leaving it to be discovered.

Built With

Share this project:

Updates

Submission history