個人開発アプリ『ユメハシ』の技術スタックと、v2.1.0 までに解決した5つの実装課題

はじめに

「個人開発って、月いくらかかるんですか?」

たまにそう聞かれます。私の答えはシンプルで、ユメハシは 1 年運用して 0 円 です。

この記事では、その「裏側」を、私自身がつまずいたポイントを含めて開きます。読みもの寄りの背景は もう一本の記事 に置いてあるので、ここでは思いっきり技術側に振ります。

対象は、ユメハシ(YumeHashi) — 夢を目標に、目標をタスクに分解して行動に変える Flutter Web アプリです。


全体アーキテクチャ

まず全体図を見てもらった方が早いと思います。

[ブラウザ]
  ├── [ブラウザ内 SQLite]  ← マスターデータ(Drift ORM + WASM)
  ├── [Firestore]          ← クラウドバックアップ(JSON 全量同期)
  ├── [Firebase Auth]      ← 匿名認証 → メール連携
  ├── [Apps Script]        ← Stripe 決済プロキシ
  └── [GitHub Pages]       ← 静的ホスティング(月額 0 円)

設計のキモは、データの主記憶をブラウザ内 SQLite に置いた ことです。Firestore は「バックアップ」という位置付けに留めてあるので、読み取り課金が DAU(デイリーアクティブユーザー)にあまり比例しません。この割り切りで、DAU 3,000 人までは無料枠に収まる試算 になりました。

「ユーザーが増えるたびに財布が痛む」設計だと、私だと続けられないと思ったんです。

主要技術スタック

選定理由はあとから理屈をつけたものも含みます。正直に言うと、Flutter は「触ってみたかった」が半分くらいです。

レイヤ技術選定理由
UIFlutter (Dart)単一コードベースで Web / Windows 対応
状態管理Riverpodコンパイル時依存解決、テスト容易
ローカル DBDrift (SQLite)型安全クエリ、WASM で Web 対応
認証Firebase Auth匿名 → メール連携のシームレス移行
決済Stripe (via Apps Script)クライアントに秘密鍵を持たせない

インフラコスト

1 年運用して、現状は 月額 0 円 をキープできています。コストが動き得るとしたら Firestore の書き込み回数で、ここの閾値が DAU 3,000 人です。

なぜ Flutter Web を選んだか

正直、最初は React + Next.js とどっちにしようか迷いました。並べるとこんな感じです。

候補メリットデメリット
Flutter Webマルチプラットフォーム、宣言的 UI初回ロードが重い
React + Next.jsWeb 最適化モバイルは別途必要

最終的に Flutter Web に倒したのは、ユメハシが タイムラインや星座ダッシュボードといった「絵」の要素を多用する からです。Web 寄りの最適化より、UI 表現の自由度を取りました。初回ロードの重さは課題が残るのを知っていましたが、これは後述の課題 #1 で潰しています。


v2.1.0 までに解決した 5 つの実装課題

ここからが本題です。どれも、机上で予測していたものではなく、運用してから気付かされた 種類の課題でした。

課題 #1: 初回アクセスが遅い(6〜10 秒)

最初に Lighthouse を回した瞬間、正直「これは公開できないな」と思いました。

HAR ファイル(ブラウザの通信ログ)で計測したところ、キャッシュありでも First Paint まで 3.4 秒。コールドスタートでは 6〜10 秒。

原因: Apps Script の Stripe 検証が 1.8 秒かかり、直列依存チェーンのブロッカーになっていた。

解決策:

  1. キャッシュされたプレミアム状態を runApp() 前に同期適用(外部通信を待たない)
  2. 外部通信を addPostFrameCallback で初回描画後に遅延実行
  3. index.html に preconnect / preload ヒントを追加

「決済の整合性を取るために、起動を遅らせていた」のは、よく考えると本末転倒でした。先に描画してから整合性を後追いする、という順番に倒した結果、体感はかなり軽くなりました。

課題 #2: 受信ボックスのデータ肥大化

運用 1 年で、受信ボックスの通知がじわじわ溜まっていきました。メモリも Firestore も、いつのまにか食われている。

解決策: 「既読かつ 30 日経過した通知は物理削除」のポリシーを入れました。未読は期間にかかわらず保護する、という非対称設計にしているのがポイントです。「未読のまま消えた」は、ユーザーから見ると一番イヤな体験だと思ったので。

課題 #3: 完了タスクの増大

10 ヶ月運用した実機で、タスク一覧の描画が体感で重くなり始めました。「気のせいかな」と思って数えてみたら、完了タスクが思った以上に積み上がっていたんです。

解決策: 完了タスクをデフォルト非表示 + 30 日経過で物理削除。DataRetentionService が起動時に 1 回実行。ユーザーへの事前告知として FAQ に「残したいデータは事前にエクスポートを」を追加しました。

「自動で消える」設計に踏み切るときは、事前のアナウンス が一番大事だと感じました。

課題 #4: 重複告知

ある時、同じ告知が二重に出ているという報告をもらいました。

原因: announcements.json から削除したエントリが、DB に残り続けていた。

解決策: JSON を「唯一のソース」として扱い、JSON にない dedup_key を持つ通知を DB から削除する処理を入れました。「ソースを 1 つに絞る」と決めるだけで、設計の不思議な揺れが消えます。

課題 #5: CI のバージョンチェックがレース条件で誤検知

これは本当に「ハマった」系でした。

原因: GitHub Actions のキュー遅延でマージ後に CI が走り、main と PR のバージョンが一致して誤判定。

解決策: git merge-base --is-ancestor で「すでにマージ済み」を検出し、version-check をスキップ。タイミング問題は、タイミングで解こうとしてはダメで、「状態」を見に行く のが正解でした。


AI 駆動開発の実績

最後に、これらをひとりで作れた理由について少しだけ。

項目数値
初回リリースまで3 週間(未経験の Flutter/Dart から)
テスト件数847 件
ソースコード行数約 30,000 行

「未経験の言語で 3 週間」は、私自身も振り返って驚いています。詳細は AI 駆動開発の比較記事 に書きました。

アプリは ユメハシ(YumeHashi) で試せます。リポジトリは GitHub で公開しています。「動いているもの」と「ソース」を両方眺めてもらうと、この記事の内容が立体的に見えるかもしれません。

もし同じような構成で個人開発を考えている方がいたら、まずは「主記憶をクラウドに置かない」だけでも、財布の景色がだいぶ変わります。

関連記事

お問い合わせ

ご質問やフィードバックなど、お気軽にお問い合わせください。

お問い合わせはこちら