子育てアプリを「PWA」で作った理由

親子で使うタスク&ごほうび管理アプリ「RingiQuest」を、ネイティブアプリではなく PWA(Progressive Web App)として作りました。個人開発でこの選択をした理由、Cloudflare Workers 1つで完結させた構成、そして実際にぶつかった制約——iOS のプッシュ通知、Web Push の自前実装、AI のコスト設計で踏んだ落とし穴までを、包み隠さず書きます。

App Store の審査を待ちたくなかった

このアプリは、自分の家族のために作りはじめたものです。中学2年生の息子の夜更かしをなんとかしたい、という個人的な動機が出発点でした(その経緯は別の記事に書いています)。

そういう作り方をすると、開発のサイクルが独特になります。夕食のときに「ここが使いにくい」と言われて、その夜に直す。翌朝には直ったものが使える。この速度が出せないなら、家族は使ってくれません。

個人開発でネイティブを選ぶコスト

ネイティブアプリでこれをやろうとすると、いくつもの関門があります。

  • iOS と Android で、それぞれ別のコードとビルド環境
  • 開発者アカウントの年額費用
  • ストアの審査(修正のたびに待ち時間が発生する)
  • ユーザー側の「インストールする」という決断

最後のひとつが、個人的にはいちばん重いと考えました。まだ価値が分からないアプリを、わざわざストアからインストールしてくれる人はほとんどいません。まして、それが知らない個人が作ったものであればなおさらです。

「明日直したい」に応えられるか

審査があると、緊急の修正にも数日かかることがあります。家族向けの小さなアプリで、ポイント計算にバグが見つかったとして、修正版が使えるようになるのが来週です、というのは受け入れがたい。

Web ならデプロイした瞬間に反映されます。この一点だけでも、選ぶ理由としては十分でした。

PWA を選んだ4つの理由

理由1: インストールの障壁がゼロ

PWA は URL です。「これ使ってみて」とリンクを送るだけで、相手はもう使えます。気に入ったらホーム画面に追加してもらえばアプリのように起動しますが、それは後からでかまいません。

一般公開したいと考えていたので、この差は決定的でした。ストアの検索結果で見つけてもらうより、記事や口コミからリンクをたどってもらうほうが、個人開発には現実的です。

理由2: iOS / Android / PC を1つのコードで賄える

主な利用端末はスマートフォンですが、保護者が PC から設定を見たい場面もあります。1つのコードベースで全部を賄えるのは、個人で保守するうえで大きい。

理由3: 更新が即時に反映される

前述のとおりです。GitHub の main ブランチにマージすれば、CI がデータベースのマイグレーションを適用してからデプロイします。数分後には本番に出ています。

理由4: ストアの手数料も審査もない

このアプリは完全無料で、アプリ内課金もありません。ストアに置く経済的な理由がそもそもありませんでした。

加えて、子ども向けのアプリをストアに出す場合、各ストアのファミリー向けポリシーへの対応が必要になります。それ自体は正しい規制ですが、個人開発で最初に向き合うにはコストが高い。Web で公開し、自分でプライバシーポリシーを整備するほうが、結果として誠実にやれると判断しました。

それでも PWA が抱えている現実的な制約

いいことばかり書いても仕方がないので、実際にぶつかった制約を書きます。PWA を検討している人は、ここだけは事前に知っておいたほうがいいと思います。

制約1: iOS のプッシュ通知は「ホーム画面に追加」が必須

これが最大の制約です。iOS では、Safari でページを開いているだけでは Web Push を受け取れません。ホーム画面に追加して、そこから起動した状態でないと、通知の許可すら求められないのです。

RingiQuest にとって通知は重要な機能です。稟議の決裁依頼が届いたこと、クエストの期限が近いこと、朝と夜の応援メッセージ。これらが届かないと、体験がかなり削られます。

対策としてできることは、案内を丁寧にすることくらいでした。使い方ガイドに iOS 向けの手順を明記し、アプリ内でも通知設定の前に案内を出しています。それでも、ここで一定数の利用者が離脱するのは避けられないと見ています。

制約2: 通知許可までの導線が長い

ネイティブアプリなら、インストール直後に通知許可のダイアログを出せます。PWA では「ホーム画面に追加 → そこから起動 → 設定を開く → 通知を有効にする」という手順を踏んでもらう必要があります。

手順が1つ増えるごとに、完了する人は減ります。ここは PWA の構造的な弱点です。

制約3: 「アプリを入れる」という一般的な感覚とのズレ

技術者には自明でも、一般の利用者にとって「ブラウザで開いてホーム画面に追加すると、アプリみたいに使える」というのは、まだ当たり前の知識ではありません。

LP に「インストール不要」と大きく書いていますが、これがメリットとして伝わるか、それとも「ちゃんとしたアプリじゃないのでは」という不安になるかは、正直まだ判断がついていません。

PWA を選ぶ前に確認すること プッシュ通知がプロダクトの中核機能なら、iOS の制約は事前に必ず検証してください。「あとで何とかなる」部分ではありません。逆に、通知が補助的な役割なら、PWA の弱点はほとんど問題になりません。

技術構成——Cloudflare Workers 1つで完結させた

構成の方針は明確でした。管理するインフラの数を、可能な限り減らす。個人開発で継続的に保守することを考えると、構成要素の数がそのまま将来の負債になります。

Workers + Static Assets で、SPA と API を同居させる

1つの Cloudflare Worker が、React の SPA を配信し、同時に /api/* へのリクエストも処理します。デプロイは wrangler deploy の一発で完結します。フロントとバックエンドを別々にデプロイする必要がありません。

新規プロジェクトでは Pages ではなく Workers が推奨されており、静的アセットの配信も Workers に統合済みです。今から始めるなら、こちらが素直だと判断しました。

データ層: D1 + Drizzle ORM

Cloudflare の SQLite ベースのデータベース D1 に、Drizzle ORM を組み合わせています。スキーマ定義から SQL のマイグレーションを生成でき、wrangler d1 migrations と噛み合います。

運用で重要だったのは、複数テーブルの更新を必ず1つのバッチで原子化することでした。ポイントの付与、クエストの完了、稟議のステータス更新。これらが途中で失敗して片方だけ書かれると、家庭内のポイント経済が壊れます。

関連して、設計上の判断をひとつ。ポイントの残高カラムを作りませんでした。残高も累計も、すべて元帳(point_ledger)から都度導出します。カラムを持つと、必ずどこかで実際の履歴とズレます。金額を扱うなら、集計値を保存しないほうが安全です。

API: Hono の RPC で型を共有する

API フレームワークは Hono です。Workers 上でネイティブに動き、非常に軽量です。

採用の決め手は RPC モードでした。サーバー側で定義した型が、そのままクライアントで使えます。コード生成のステップを挟まずに、API の型安全性が得られる。個人開発では、この「手順が1つ減る」ことの価値が大きい。

バリデーションは Zod で書き、スキーマを client と server で共有しています。API の仕様が変わったとき、直す場所が1か所で済みます。

選ばなかった構成と、その理由

  • Next.js。 アプリ本体は認証必須なので、SSR も SEO も要りません。Edge Runtime の制約に付き合うコストに見合いませんでした。一般公開用のランディングページは、こうして静的ページとして別に作れば足ります。
  • SSR 前提のフレームワーク全般。 成立はしますが、今回の要件には抽象化が過剰でした。Vite と Hono の組み合わせのほうが薄く、速い。
  • モノレポツール。 パッケージが1つで足りる規模です。client / server / shared のディレクトリ分割で十分に関心を分離できました。

Web Push を自前で実装することになった話

この記事でいちばん技術的に泥臭い部分です。

既存ライブラリが Workers 上で動かなかった

Web Push を実装しようとして、既存のライブラリを順に試しました。結果は全滅でした。Node.js の暗号 API に依存しているものが多く、Workers のランタイムでは動きません。

選択肢は2つ。通知機能をあきらめるか、自分で書くか。通知はこのアプリの中核だったので、書くことにしました。

RFC 8291 の暗号化を書く

Web Push のペイロードは、RFC 8291 で定められた方式(aes128gcm)で暗号化して送る必要があります。鍵の共有、暗号化のパラメータ、ヘッダーの組み立て。仕様を読みながら実装しました。

こういうコードのつらいところは、間違っていても「送信は成功したように見える」ことです。暗号化がわずかにずれていても、リクエスト自体は通ります。ただ、通知が届かない。原因の切り分けが極端に難しくなります。

参照実装とのラウンドトリップテストで担保する

そこで、テストの方針を決めました。参照実装との相互運用性をテストで固定することです。

自分で暗号化したものを参照実装で復号できるか、逆に参照実装が暗号化したものを自分の実装で復号できるか。この往復が通ることを、テストとして常に走らせています。自分の実装同士でテストしても意味がありません——間違い方が一貫していれば、間違ったまま通ってしまうからです。

個人開発で暗号まわりを自前実装するのは、基本的におすすめしません。ただ、どうしても必要なら、参照実装との突き合わせだけは必ずやってください。

AI のコスト設計で踏んだ落とし穴

最後に、つい最近見つけた問題を書きます。個人開発で AI を使うなら、たぶん役に立ちます。

可用性のための設計が、コストの穴になっていた

このアプリには AI を使った機能がいくつかあります(対価ポイントの提案、応援メッセージ、クエスト提案、週次レポート、レートの見直し提案)。実行基盤は Workers AI です。

AI のモデルは予告なく廃止されることがあります。実際、開発中に一度その事態に遭遇しました。そこで、モデルを複数並べたフォールバックチェーンを組み、第1候補が使えなければ次を試すようにしました。可用性の設計としては、ごく普通です。

問題は、そのチェーンの2番目に、約6倍高価なモデルを置いていたことでした。

「請求書でしか気づけない」状態になっていた

第1候補が廃止されると、システムは自動的に高価なモデルへ落ちます。機能は正常に動き続けます。エラーも出ません。画面上、何も変わりません。

つまり、コストが数倍になったことに気づく手段が、月末の請求書しかない状態でした。可用性のために入れた仕組みが、そのままコスト事故の導線になっていたわけです。

どう直したか

対処は3つです。

  1. チェーンから高価なモデルを完全に外した。 「最後の砦」としても残しませんでした。このアプリの AI 機能はすべて決定論的なフォールバック(計算式や定型文)を持っているので、AI が使えないことは、すでに設計された正常な縮退です。安価なモデルが全部落ちている状況は、6倍払う場面ではありません。高価なモデルを最後に置くこと自体が、リスクの再導入でした。
  2. 第1候補以外が応答したら、警告ログを出すようにした。 これが本質的な修正です。従来は全モデルが失敗したときしかログが出ず、第1候補が死んで第2候補が答えている状態は無記録でした。
  3. 単価の上限を、単体テストで機械的に固定した。 チェーンに入っている全モデルが単価表に載っていること、上限以下であることをテストで検査します。コメントに「高いモデルを入れるな」と書くだけでは、いつか誰か(未来の自分)が入れます。

教訓 フォールバックは「動き続けること」を保証しますが、「同じコストで動き続けること」は保証しません。縮退したことが観測できない設計は、壊れていないのと同じではありません。フォールバック先の単価は、可用性の設計と同時に決めるべきでした。

PWA で個人開発をする人へ

向いているケース

  • リンクを送って試してもらう形で広めたい
  • 頻繁に更新したい、すぐ直したい
  • iOS / Android / PC を1人で保守したい
  • 課金がない、またはストア外決済で問題ないもの

向いていないケース

  • プッシュ通知が体験の中核で、iOS の利用者が多い
  • カメラ・Bluetooth・バックグラウンド処理などを深く使う
  • ストアの検索流入を主な集客経路にしたい

最初に決めておくとよいこと

経験から3つ挙げます。

  • Service Worker の検証方法。 開発サーバーでは Service Worker が無効になっている構成が多く、通知やオフライン動作は本番相当のビルドでしか確認できません。この検証フローを最初に確立しておかないと、後半で必ず詰まります。
  • 外部サービスが未構成でもアプリが動くこと。 RingiQuest では、通知の鍵も、AI も、メール送信も、未設定のまま全機能が正常に動きます。これは開発とテストの速度に直結しました。「あの環境変数がないと起動しない」は、個人開発では思った以上に足かせになります。
  • データを持ち出せる仕組み。 利用者のデータを預かるなら、エクスポートと削除は最初から入れておくべきです。後から足すと、テーブルが増えるたびに漏れます。

まとめ——技術選定は、誰に届けたいかで決まる

PWA を選んだのは、技術的に優れているからではありません。「リンクを送るだけで使える」という性質が、このアプリの届け方に合っていたからです。

知らない個人が作った子育てアプリを、わざわざストアからインストールしてくれる人は多くありません。でも、記事を読んで興味を持った人がリンクをタップして、その場で試してみることはあります。この差を取りに行くために、iOS の通知まわりの制約は受け入れる価値がありました。

技術選定に一般解はありませんが、判断の軸ならあります。誰に、どういう経路で届けたいか。それが決まっていれば、選択肢は自然に絞れます。

RingiQuest を触ってみる この記事で書いた構成で動いているアプリです。ブラウザで開けばそのまま使えます。完全無料・アプリ内に広告なし。

RingiQuest を無料で試す