-
-
One hive, every subscription. $93.26 a month across 10 services the number most people never actually add up.
-
Sorted by what renews next, so the nearest charge is always first. A reminder lands before each one, while cancelling still works.
-
Two taps to add a service or screenshot your subscriptions page and let the app read it. On-device, no bank login, no account.
🐝 Inspiration
I opened my App Store subscriptions page one evening expecting four entries. There were nine. Two were trials I'd meant to cancel months earlier, one was a service I'd stopped using in March, and the annual one was set to renew in eleven days.
Nothing about that was an accident of my own carelessness. A hive has drones: they don't forage, don't make wax, don't defend the colony they just eat honey. Come autumn the workers walk them out. Forgotten subscriptions are drones, and almost nobody does the autumn walk, because each one is individually too small to investigate and collectively large enough to matter.
So I went looking for something to do the walk for me, and found the category had a credibility problem:
Most subscription trackers are subscriptions. You pay monthly for an app whose entire argument is that you pay for too many things monthly. Most of the rest want your bank credentials so they can read your transactions asking people already worried about money to hand a stranger their full financial history in order to find out they're wasting some of it.
Both felt like the wrong trade for a bookkeeping problem. SubscriBee is the app I wanted to exist: one hive, every subscription, one payment ever, and nothing leaving the phone.
🍯 What it does
SubscriBee puts every subscription you pay for into one comb and tells you what the swarm actually costs.
Two taps to add a service. A built-in catalogue with real logos, so most entries are a search rather than a form. Import from a screenshot. Screenshot your App Store or Google Play subscriptions page and SubscriBee reads it names, prices, renewal dates using on-device text recognition. No bank login, no account linking, and the image never leaves the device. A nudge before each sting. Local notifications ahead of every renewal, at an hour you choose, while cancelling is still an option. Analytics that answer a real question. Monthly and yearly totals, spend by category, and what the next 30 days will cost. 32 languages, including right-to-left, with currency and date formatting that follows the device rather than the app. iCloud sync across your own devices, off by default.
Five subscriptions are free forever, reminders included. SubscriBee Pro is one payment, once unlimited subscriptions, screenshot import, advanced analytics, custom categories, export/import, and sync. A tracker billed monthly would just be another drone.
🔧 How we built it
One codebase, both stores: Expo SDK 57 / React Native 0.86 on the New Architecture, with Continuous Native Generation, so ios/ and android/ are build artefacts rather than checked-in code.
Layer Choice Money RevenueCat offerings, purchase, entitlement, restore, on both platforms Drawing Skia for the honeycomb and charts; Reanimated on the UI thread for motion Reading screenshots ML Kit text recognition, entirely on-device State Zustand over MMKV synchronous, offline, no network in the data path Routing Expo Router Copy i18next across 32 locales
RevenueCat carries the whole commercial surface, and one rule shapes the rest of the app: the store is the source of truth for entitlement, never the app. The local premium store is a synchronous mirror written by exactly one function, re-synced on every purchase, restore and customer-info callback. A refund or a family-sharing revoke closes the door again on its own. When the store can't be reached, the paywall shows the built-in price and labels it as indicative rather than quoting a number it can't stand behind.
🪲 Challenges we ran into
Four subscriptions became seven. The screenshot parser split one Play entry across several. My first fix assumed OCR line height marked headings it didn't work, twice. So I stopped guessing, ran the actual screenshot through a local OCR and measured the pixel boxes. The assumption was backwards: body lines measure taller than titles, because bounding boxes include ascenders and descenders, and section headings were tallest of all. Cap height is not a signal. Vertical gaps are. The parser now clusters on gap distance, and the real measured boxes from that screenshot are frozen into the test suite so the mistake can't come back.
The Pro gate was invisible on Android. expo-blur defaults to blurMethod: 'none' there — it renders a semi-transparent view and no blur at all, so white text sat on top of white content. Rather than switch on the experimental blur, the gate now draws a deterministic scrim with its own colour token per theme. Predictable beats fancy when legibility is the requirement.
Android quietly ate menu options. React Native's Alert.alert does buttons.slice(0, 3) on Android and passes cancelable: false. A four-option menu silently lost an option, and the dialog couldn't be dismissed. Replaced with a bottom-sheet action host which also turned out to let a nested sheet swap its content in a single React batch instead of closing and reopening a modal.
A correct password reported as incorrect. Gradle insisted the keystore password was wrong. It wasn't. gradlew is a batch file, so every argument travels through cmd.exe, which reads > as redirection — and the generated password contained one. It was being truncated before Gradle ever saw it. Quoting doesn't help; those characters are interpreted earlier. The fix was to hand Gradle a properties file, with no shell in between, written before the build and stripped in a finally.
A build script broken by a dash. PowerShell 5.1 reads a BOM-less .ps1 in the system codepage. An em dash in a comment decoded under CP1252 into three characters whose last one, ", PowerShell happily accepted as a closing quote producing a parse error 270 lines from the actual cause. The script is pure ASCII now.
statusCode=3. Android purchases failed with RevenueCat's configuration error. The dashboard was correct, the key was correct, the product was published. The logcat showed Play returning BILLING_UNAVAILABLE for our product and, twelve minutes earlier in the same log, doing exactly the same thing to a completely unrelated app from a different developer. That second app was the proof: the problem was the build not being one Play recognised, not our configuration. Nothing in the code needed to change.
Giving reviewers a way past the paywall, without breaking the paywall. Store review needs access to gated features without a purchase. The obvious implementation set the Pro flag dies within a second: the customer-info listener sees no entitlement and revokes it. Review access had to be a separate flag that the selectors OR together, so the store owns its field and the unlock owns its own. The code itself never ships; only its SHA-256 does, so pulling apart the binary yields a digest and not a key.
🏆 Accomplishments that we're proud of A subscription tracker that isn't a subscription. One payment, no ads, no upsell loops, no engagement mechanics. No servers, no accounts, no telemetry. The app works with the network off. Restore still crosses a user's devices, because RevenueCat carries identity that we never have to. Finding subscriptions without bank access. Screenshot import reaches the same answer as transaction-reading competitors, on-device, with nothing transmitted. 32 languages with a test that fails the build if any user-visible string hardcodes a platform's store name, so Android users are never told to check their App Store subscription. One codebase, two platforms, with platform differences treated as design decisions rather than papered over. 📚 What we learned
Measure, don't theorise. The OCR bug cost two failed fixes built on a plausible assumption and took fifteen minutes to solve once the real pixel boxes were on screen. Every hypothesis before that was reasonable and wrong.
Cross-platform means the differences are load-bearing. A blur that isn't a blur, a dialog that drops your fourth button, a shell that eats your password none of these are cosmetic, and none surface in a simulator. They surface in the build a user installs.
Entitlement belongs to the store. Making the app a mirror rather than a keeper of Pro status meant refunds, family sharing, restores and multi-device all worked without a single special case and it's why review access needed its own field instead of borrowing that one.
The log usually knows. Two separate days of guessing about billing ended with one adb logcat that named the answer, including a stranger's app in the same log acting as the control group.
🚀 What's next for SubscriBee Android launch built, signed, and in review as of this submission. More import sources: renewal and receipt emails, and bank statement exports parsed on-device. Home-screen widgets for the next charge and the monthly total. Shared hives household subscriptions split between people, still without accounts. Cancellation help: deep links straight to the right cancellation screen for the services that hide it.
Built With
- android
- cross-platform
- expo-notifications
- expo-router
- expo.io
- i18next
- in-app-purchase
- ios
- kotlin
- ml-kit
- mmkv
- new-architecture
- ocr
- offline-first
- on-device-ml
- privacy
- react-native
- react-native-purchases
- reanimated
- revenuecat
- skia
- swift
- typescript
- zustand
Log in or sign up for Devpost to join the conversation.