Skip to content
nomunomu0504.dev

BLOG #REACT-NATIVE

Cherimo: rebuilding an app for the small things from usage data

Hiroki Nomura 5 MIN READ react-native expo supabase indie-dev
On this page

What it is

Their favourite things and the things they dislike, anniversaries, the gifts you gave and how they landed, the thing they mentioned wanting in passing, the places you went together. The small things you do not want to forget and do anyway, organized per person. From what you have saved, an AI suggests gifts, date plans and thoughtful messages.

It is not a shared couples’ app for two. You use it alone. There is no invitation, and nothing you record is ever shown or sent to the other person. The person does not have to be a partner: family, children and friends all fit.

Three Cherimo app screens
From left: home, anniversaries and reminders, the AI assistant

How it is built

A Turborepo monorepo holds the web and mobile apps together.

  • apps/web: Next.js (App Router) on Vercel. The marketing pages, every signed-in screen and the API routes
  • apps/mobile: Expo SDK 58 / React Native 0.88, for iOS and Android
  • packages/shared: Japanese and English i18n and the zod schemas

Supabase is the backend: Postgres with RLS on every table, Apple and Google sign-in, Storage for images. The AI is Claude through the Vercel AI SDK, and monthly AI usage is counted from the log table rather than a dedicated counter. Anniversary reminders are decided by a Vercel Cron job and sent as Web Push and mobile push. The home-screen widgets, @bacons/apple-targets on iOS and react-native-android-widget on Android, show the days until and since a chosen anniversary.

Billing runs on two rails, Stripe on the web and RevenueCat on mobile, but the single source of truth for entitlements is one column, users.plan. Screens and API routes never read Stripe tables or RevenueCat subscription state directly. Whichever rail a purchase comes through, updating that one column makes it count.

Cherimo billing: one column decides the planApp StoreGoogle Playin-app purchaseRevenueCatwebhookWeb checkoutcherimo.appStripewebhookusers.planfree · plus · prothe only value the UI readsExpo app · web · widgets · AImaxPartners · AI quota · RLSStripe tables and RevenueCat state are never read directly by screens or APIs.
Two payment routes, one column that decides the plan

Development and production are separate Supabase projects, with environment variables synced from Infisical to Vercel. Writes to the production database are blocked by an agent hook, so Claude Code cannot touch production by mistake.

What the usage data said

A few months after launch, I sat down with the production logs. My standing hypothesis was that people were dropping out because data entry was tedious. It was wrong. A high share of users created a person and went on to record preferences; onboarding was working.

The drop-off happened on the day of sign-up. People entered everything on day one and never came back. And anniversaries averaged a little over one per user, with a maximum of two. Nobody was anywhere near the free limit of ten.

The cause was the data model. With only one person allowed, you enter a birthday and the day you got together, and that is it. Notifications fire once or twice a year. For the other 363 days the app has nothing to say. Of the eleven AI features, about half were ever used.

The conclusion was that the thing to build was not another AI feature. It was support for more than one person.

What 1.4.0 did

  • Dropped the unique constraint on partners.user_id so a user can have several people
  • Found and fixed everything that assumed one person: an avatar update overwriting everyone, failing upserts, .single() throwing on multiple rows
  • Made records save to the currently selected person. Anniversaries, preferences, gifts, notes and AI output had all been pinned to the first person, so records for anyone else could not be created
  • Made the home screen and widgets span everyone, listing all anniversaries soonest first
  • Added a weekly note that resurfaces something they like, as a daily cron job with no AI involved
  • Fixed data export, which had only ever written one person’s data
  • Added maxPartners to the plans (3 on Free, 10 on Plus, unlimited on Pro)

Before that, 1.2.0 was a full accessibility pass: every screen usable with VoiceOver and TalkBack, labels on icon-only buttons, and the widget reading as one natural sentence.

What I learned

  • Decide what to build from the distribution in the raw logs, not from dashboard averages. “Maximum two” explained the structure where “average one point one” did not
  • Keep entitlements in one column. Add payment rails without adding places that decide
  • Ship invisible updates such as an Expo SDK upgrade as their own patch release (1.4.5), not mixed with visible changes, so the build that passed device testing is the build that ships
The cherimo.app landing page
cherimo.app, where the web version lives

Cherimo is on iPhone, Android and the web. cherimo.app

  1. LaterHub: turning read-later into team knowledge 4 min
  2. minnama.beer: a map that finds bars by the beer on tap 4 min
  3. Stacky: an iOS clipboard app designed from the OS constraints 6 min
Hiroki Nomura @nomunomu0504 · CTO, MIRAI STUDIO

Flutter, AWS, homelab. A few side projects and open-source tools. For architecture questions or collaboration, reach out via X DM or the About page. About

COMMENTS · GISCUS