個人開発アプリ『ユメハシ』の技術スタックと、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 は「触ってみたかった」が半分くらいです。
| レイヤ | 技術 | 選定理由 |
|---|---|---|
| UI | Flutter (Dart) | 単一コードベースで Web / Windows 対応 |
| 状態管理 | Riverpod | コンパイル時依存解決、テスト容易 |
| ローカル DB | Drift (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.js | Web 最適化 | モバイルは別途必要 |
最終的に 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 秒かかり、直列依存チェーンのブロッカーになっていた。
解決策:
- キャッシュされたプレミアム状態を
runApp()前に同期適用(外部通信を待たない) - 外部通信を
addPostFrameCallbackで初回描画後に遅延実行 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 で公開しています。「動いているもの」と「ソース」を両方眺めてもらうと、この記事の内容が立体的に見えるかもしれません。
もし同じような構成で個人開発を考えている方がいたら、まずは「主記憶をクラウドに置かない」だけでも、財布の景色がだいぶ変わります。
関連記事
- 『ユメログ』から『ユメハシ』へ — 夢と現実のあいだに、橋を架ける — アプリ改名の経緯と開発哲学
- Firestore 無料枠を先延ばしするためにやった 4 つのこと — gzip 圧縮・デバウンス・format バージョニング
- AI 駆動開発 vs 従来開発の全工程比較 — 3 週間でアプリを作った実績データ