本文へスキップ
nomunomu0504.dev

BLOG #REACT-NATIVE

Cherimo: 大切な人の「小さなこと」を覚えておくアプリを、利用データから作り直した話

Hiroki Nomura 6 MIN READ react-native expo supabase indie-dev
目次

作ったもの

相手の好きなもの、苦手なもの、記念日、贈ったものとその反応、会話の中でさりげなく言っていた欲しいもの、一緒に行った場所。忘れたくないのに忘れる「小さなこと」を、人ごとに整理しておくアプリ。保存した情報をもとに、AI がギフトやデートプラン、気遣いのメッセージを提案する。

2 人で使う共有型のカップルアプリではなく、自分ひとりで使う。相手を招待する機能はなく、記録が相手に通知されることも見えることもない。相手は恋人に限らず、家族でも子どもでも友人でもいい。

Cherimo のアプリ画面 3 枚
左から、ホーム / 記念日とリマインダー / AI アシスタント

構成

Turborepo のモノレポで、Web とモバイルを一緒に持っている。

  • apps/web。Next.js(App Router)、Vercel。マーケティングページとログイン後の全画面、API ルート
  • apps/mobile。Expo SDK 58 / React Native 0.88。iOS と Android
  • packages/shared。日英の i18n と zod のスキーマ

バックエンドは Supabase。Postgres に RLS を全テーブルで掛け、認証は Apple と Google、画像は Storage。AI は Claude を Vercel AI SDK 経由で呼び、月あたりの AI 利用回数は専用のカウンタを持たず、ログテーブルの当月件数を数える。記念日のリマインドは Vercel Cron で判定して、Web Push とモバイルのプッシュに流す。ホーム画面のウィジェットは iOS が @bacons/apple-targets、Android が react-native-android-widget で、選んだ記念日までの残り日数と経過日数を出す。

課金は Web が Stripe、モバイルが RevenueCat の二系統だが、権限の真実は users.plan の 1 列に寄せた。Stripe のテーブルや RevenueCat の購読状態を画面や API から直接見ない。どちらの経路で買っても、この 1 列が更新されれば同じ扱いになる。

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.
課金は 2 系統でも、権限の判定は users.plan の 1 列だけ

開発と本番は Supabase のプロジェクトごと分け、環境変数は Infisical から Vercel に同期している。本番 DB への書き込みはエージェントのフックでブロックしていて、Claude Code が間違って本番に触れないようにした。

利用データが教えてくれたこと

リリースして数か月たった頃、本番のログを素直に眺めた。先に立てていた仮説は「入力が面倒で途中で離脱している」だった。外れていた。相手を登録し、好みまで入れる人の割合は高く、オンボーディングは機能していた。

離脱は登録した当日に起きていた。初日に全部入れて、二度と戻ってこない。そして記念日は 1 人あたり平均 1 件台、最大でも 2 件。無料枠の 10 件には誰も近づいていなかった。

理由はデータ構造にあった。相手を 1 人しか登録できないので、入るのは「誕生日」と「付き合った記念日」で打ち止め。通知は年に 1〜2 回。残りの 363 日、アプリから言うことが何もない。AI 機能は 11 個用意していたが、使われていたのは半分ほどだった。

結論は、作るべきは新しい AI 機能ではなく複数人対応だ、ということだった。

1.4.0 でやったこと

  • partners.user_id の unique 制約を外し、1 ユーザーが複数の相手を持てるようにした
  • 単一前提で壊れる箇所を洗い出して直した。アバターの更新が全員を上書きする、upsert が失敗する、.single() が複数行で落ちる、など
  • 記録の保存先を「いま選んでいる相手」にした。記念日・好み・ギフト・メモ・AI の保存先が先頭の相手に固定されていて、2 人目以降の記録が作れない状態だった
  • ホームとウィジェットを全員横断にして、登録したすべての人の記念日が近い順に並ぶようにした
  • 保存した「好き」を週に一度そっと知らせる通知を日次の cron に足した。AI は使わない
  • データのエクスポートが 1 人分しか出ていなかったので全員分に
  • プランに maxPartners を足した(無料 3 人、Plus 10 人、Pro 無制限)

その前の 1.2.0 ではアクセシビリティを全面的に見直している。VoiceOver / TalkBack で全画面を操作できるようにし、アイコンだけのボタンにラベルを付け、ウィジェットは「◯◯まで あと◯日」と 1 文で読み上げるようにした。

学び

  • 何を作るかは、ダッシュボードの平均値ではなく生ログの分布を見て決める。平均 1 件より「最大 2 件」が構造を教えてくれた
  • 課金の権限は 1 列に寄せる。支払い経路が増えても判定の場所は増やさない
  • Expo SDK の更新のような「見えない更新」は、見える変更と混ぜずに単独の patch で出す(1.4.5)。実機確認が通った状態をそのまま出せる
cherimo.app のトップページ
cherimo.app。Web 版もここから

Cherimo は iPhone、Android、Web で使える。cherimo.app

  1. LaterHub: 「あとで読む」をチームのナレッジにするために作ったもの 5 min
  2. みんなの生ビールマップ: 「銘柄から店を探す」だけに絞った地図アプリの作り方 4 min
  3. Stacky: iOS でクリップボード履歴アプリを作るとき、OS の制約から逆算した話 7 min
Hiroki Nomura @nomunomu0504 · CTO, MIRAI STUDIO

Flutter / AWS / 自宅サーバー。個人開発と OSS をいくつか。設計相談や協業の話は X の DM か About から。 About

COMMENTS · GISCUS