わが家の脱・料理属人化プロジェクト、基本機能編の 1 回目です。
導入編では、夫婦で交代しながら料理をしているのに、レシピやコツが片方の頭の中にしかない、という困りごとと、それを解消するために作ったアプリの概要を書きました。
基本機能編では、困りごと毎に機能と実装を紹介していきます。
その前に、今回はアプリの全体像を説明します。
この後の回では、Firestore、Cloudflare Workers、D1、共有のパッケージのコードが何度も出てきます。
それぞれが何を受け持っているのかを先に押さえておくと、コードが何のためのものか追いやすくなります。
制約: 運用コストは極力かけない(0 円が理想)
導入編で挙げた制約のうち、構成をいちばん大きく左右したのはお金です。
家族のアプリに運用コストは払いたくありません。
たとえば月に 500 円かかると、12 か月で 6,000 円 = 子どものおもちゃ 2 つは買えます。
これでは家計の承認フローはなかなか通りません。
また、何かの間違いで請求が来ないよう、クレジットカードも登録しないことにしました。
この制約を守るために、以下は使わないことにしました(いずれもアプリを作った時点の情報です)。
- Firebase の Blaze プラン(従量課金)の機能: Cloud Storage、Cloud Functions、Firestore のバックアップ
- Cloudflare R2(オブジェクトストレージ): 無料枠の範囲でもカードの登録が必要
基盤の選定
もう 1 つの条件は、オフラインでも使えることです。
買い物リストは、電波の弱い店の中でも使いたいからです。
ただし、データの同期とオフライン対応を自作するのは避けたいところでした。
| 選択肢 | 採用しなかった理由(作った時点) |
|---|---|
| Firebase(Blaze プラン)で一式そろえる | Storage と Functions にカードの登録が必要 |
| Cloudflare だけで作る | 同期の仕組みを自作する量が多い |
| Supabase など | 無料プランは使われない期間が続くと停止する。オフライン対応の構成が安定版でなかった |
| オフライン同期が売りの新しい BaaS | オフラインでの書き込みに対応していない、提供の先行きが分からない、など |
結局、データ・認証・オフライン同期は Firebase の無料プラン(Spark)に、Firebase だけでできないことは Cloudflare の無料プランに任せる、という分担にしました。
なお、Firestore は無料枠を超えても課金されず、枠が戻るまで操作が失敗するだけです。
家族だけで使うアプリなので、上限を超えることはまずありませんが、
仮に一時的に止まっても、大きな問題にはならないのでこの構成にしました。
全体像
下の図は、導入編の構成図に、端末の中の保存と定期実行を書き足したものです。
| 担当 | 仕事 |
|---|---|
| Firebase Authentication | Google サインイン |
| Firestore | レシピ・買い物リスト・家族の情報。オフライン保存、リアルタイム同期 |
| Security Rules | 家族ごとの分離、招待コードでの参加、書いた人とサーバーの時刻の確認 |
| Cloudflare Workers(Hono) | 画像の保存・配信、最後の 1 人が抜けるときの削除、通知、要望・不具合の受付、招待リンクの案内ページ |
| Workers の Cron | 毎日の掃除(削除したレシピ・使われていない画像)、毎分の通知の送信 |
| Cloudflare D1 | 画像、通知の待ち行列、端末の通知のトークン、要望・不具合の記録、バックアップ |
| GitHub Actions | 毎日のバックアップ(Workers の CPU 時間に収まらないため) |
アプリは、普段は Firestore と直接やりとりします。
画像、通知、要望・不具合の受付、管理者の権限が要る処理など、Firebase だけではできない処理だけ、Workers を利用します。
操作ごとの流れ
構成図だけでは動きが見えにくいので、よくある操作で追ってみます。
レシピを保存する
- アプリが入力を zod で確かめる
- Firestore に書き込む(React Native Firebase が、まず端末のキャッシュに書き、つながっていればサーバーに送る)
- Security Rules が、家族のメンバーか・書いた人が本人か・時刻がサーバーの時刻かを確かめる
- ほかの家族の端末に、変わったレシピだけが届く(差分同期)
ここに Workers は出てきません。
データの読み書きは、アプリと Firestore だけで完結します。
写真を付ける
- アプリが写真を縮小し、一覧用と詳細用の 2 つの大きさを作る
- Workers に送る。Workers は Firebase の ID トークンを確かめ、D1 に保存して画像の ID を返す
- レシピには、画像の ID だけを書く
画像を見るときも、Workers が毎回 ID トークンを確かめ、家族のメンバーかどうかを確認してから返します。
買い物リストに足す
- アプリが Firestore に、追加ごとに 1 ドキュメントを書く
- 書き込みがサーバーに届いたら、アプリが Workers に「足した」と知らせる
- Workers は D1 に溜めておき、毎分の Cron で、ほかの家族の端末にまとめて通知を送る
Firestore の書き込みをきっかけにサーバーで処理する仕組み(Cloud Functions)は、無料プランでは使えません。
そのため、書き込んだ端末が自分で Workers を呼びに行く形にしています。
夜中に動くもの
毎日、夜中に次の 2 つが動きます。
- GitHub Actions が Firestore を全件読み、D1 にバックアップする
- Workers の Cron が、削除の印が付いてから一定の日数が経ったレシピと、使われていない画像を消す
データの置き場所
Firestore
レシピや買い物リストなどの家族のデータは、すべて households/{hid} の下に置き、Security Rules で「この家族のメンバーか」を 1 か所で判断できるようにしています。
D1
レシピの写真、買い物リストの通知、要望・不具合の記録など、Firestore に置けないもの、置かないほうがよいものは D1 に置いています。
端末
料理モードで開いているタブ、タイマー、レシピの下書きなど、家族と共有しないものは、MMKV で端末の中にだけ置いています。
リポジトリの構成
bun workspaces と turbo のモノレポにしています。
| 場所 | 中身 |
|---|---|
apps/mobile |
Android アプリ(Expo、expo-router)。画面は src/app、機能ごとのコードは src/features、共通の部品は src/shared
|
apps/server |
Cloudflare Workers(Hono)。API と Cron、バックアップのスクリプト |
apps/web |
Web アプリ(作成中) |
packages/core |
画面に依存しない共有のコード。zod の定義、表記ゆれの正規化、まとめ貼り付けの分割、買い物リストのまとめ、バージョンの比較など |
firebase |
Security Rules とインデックス、そのテスト |
まとめ
身内だけで使うアプリなら、無料プランの組み合わせでもある程度は作れます。
家族や親しい人たちのためのアプリを作りたいと思っている方々の参考になればうれしいです。
次回からは、困りごと毎の機能の話を予定しています。