- Nuxt
- Supabase
- Cloudflare
- Stripe
- Finoko
Nuxt 4 + Supabase + Cloudflare で家族向けアプリを一人で作って運用する構成 — Finoko の技術選定
子どものお手伝い・お小遣い・金融学習アプリ「Finoko」を、企画から開発・運用・課金まで一人で回すために選んだ構成。SPA + Cloudflare Pages、Supabase の RLS と RPC、Web Push と Edge Functions、Stripe、テスト 290 件、ランニング費用の抑え方。
公開: 約 10 分namespace
Finoko(フィノコ) は、小中学生向けの「お手伝い × お小遣い × 金融学習」アプリです。子どもが家のお手伝いを報告し、保護者が承認すると残高が増える。貯めたお小遣いは「つかう」「あげる」ことができ、クイズや疑似投資でお金の感覚を学べます。
企画・UI 設計・開発・運用・課金・法務ページの整備まで、すべて一人でやっています。この記事では「一人で作って、一人で運用し続ける」ために選んだ構成と、その理由を書きます。
全体像
| レイヤー | 採用したもの |
|---|---|
| フロントエンド | Nuxt 4(ssr: false の SPA)+ Vue 3.5 + Nuxt UI 4 + Tailwind CSS v4 + Pinia |
| バックエンド | Supabase(PostgreSQL + RLS + Realtime + Storage + Edge Functions) |
| ホスティング | Cloudflare Pages(main への push で自動デプロイ) |
| 通知 | Web Push(VAPID)。notifications テーブルへの INSERT を Database Webhook が拾い、Edge Function が送信 |
| 認証 | Supabase Auth(Google OAuth のみ) |
| 課金 | Stripe(サブスクリプション。Checkout + Webhook) |
| 定期処理 | pg_cron |
| テスト | Vitest 137 件 / Playwright E2E 153 件 |
共通しているのは「常時稼働のサーバーを持たない」ことです。運用しているのが一人なので、深夜にサーバーが落ちても対応できません。落ちる要素を最初から持たない構成にしました。
なぜ SSR ではなく SPA なのか
Nuxt は SSR が売りですが、Finoko は ssr: false で SPA としてビルドしています。理由は単純で、ほぼ全ての画面がログイン後の画面だからです。検索エンジンに見せたいのはランディングページと法務ページくらいで、そこは静的に出せば済みます。
SPA にすると Cloudflare Pages に dist/ を置くだけで動き、Worker のコールドスタートやサーバー側の状態管理を考えなくてよくなります。PWA(ホーム画面に追加して使う)とも相性がいい。ログイン後のアプリを作るなら、SSR を使わない選択肢を最初に検討する価値はあります。
Supabase:RLS を軸に、重要な処理は RPC に閉じ込める
データは PostgreSQL の Row Level Security で「家族(family)」単位に分離しています。保護者は複数の家族に所属でき、子どもは自分の家族のデータしか見えません。RLS のポリシーは「誰が・どのテーブルの・どの行を・何できるか」をすべて DB 側で強制するので、クライアントのコードにバグがあってもデータは漏れません。
一方で、残高の更新のような「間違えると困る処理」はクライアントから直接テーブルを書き換えさせず、RPC 関数にまとめています。「タスクを承認する → 残高が増える → 履歴が残る → 通知が飛ぶ」を 1 つのトランザクションにしておけば、途中で落ちて残高だけ増えた、といった事故が起きません。実装用の関数は __impl のような接尾辞で分け、直接呼べないよう REVOKE しています。
Realtime は、保護者が承認した瞬間に子どもの画面の残高が変わる、という体験のために使っています。ポーリングで済ませることもできますが、子どもが「承認された!」を目の前で見られるかどうかは、このアプリでは体験の核なので。
通知:Web Push + Edge Functions
「報告が来た」「承認された」を届けるために、Web Push を使っています。仕組みは次の通りです。
- アプリ側の処理が
notificationsテーブルに 1 行 INSERT する - Supabase の Database Webhook がその INSERT を拾って、Edge Function
send-pushを呼ぶ - Edge Function が VAPID で Web Push を送る
アプリ本体は「テーブルに書く」だけで、送信処理を知りません。通知の送り方を変えたくなったら Edge Function だけ直せばよく、通知が失敗してもアプリの処理は完了しています。iOS は 16.4 以降で PWA の Push に対応しているので、ホーム画面に追加してもらえば iPhone でも届きます。
Stripe:Checkout と Webhook だけで課金を回す
Premium プランはサブスクリプションで、Stripe Checkout に飛ばして決済し、checkout.session.completed / customer.subscription.created / updated / deleted の Webhook を Edge Function で受けて契約状態を DB に同期しています。
技術より時間がかかったのは法務です。特定商取引法に基づく表記、利用規約、プライバシーポリシーを整備し、法務レビューを受けてから課金を開始しました。個人事業でも Web で完結するサブスクを売る以上、表記の省略はできません。ここは技術の話ではないので別の機会に書きます。
tax_behavior(内税 / 外税)は価格を作った後に変更できません。最初に決めておくこと。 テスト:一人で回すからこそ 290 件
単体テスト(Vitest)137 件と E2E テスト(Playwright)153 件があります。E2E は「テスト用の家族を作る → 子どもを追加 → タスクを作る → 報告 → 承認 → 残高を確認」のようにシナリオを直列に流します。
一人開発だとテストを書く時間が惜しくなりますが、逆です。レビューしてくれる人がいないので、テストが唯一の「自分以外の目」になります。本番で家族が毎日使っているアプリなので、残高が狂うバグを出すわけにはいきません。
環境も dev / prod の 2 つの Supabase プロジェクトに分け、マイグレーションは必ず dev に先に適用します。PR ベースのワークフローで、main への push がそのまま本番デプロイです。
ランニング費用を限りなく抑える
静的配信 + マネージド DB + サーバーレス関数の構成なので、固定費はほとんどありません。加えて、
- 画像は WebP 化し、投資機能で使う絵は 31MB → 1.63MB まで圧縮してから Storage に置く
- 履歴の保持期間を設計し、無料プランでは古い履歴を削除できるようにしてストレージが増え続けないようにする
- アイコンはクライアントバンドルに焼き込み、実行時に外部 API へ取りに行かない(オフラインでも表示が崩れない)
利用者が増えても固定費がほぼ増えない形にしておくと、「儲からないから閉じる」ではなく「使われている限り続ける」判断ができます。
この構成が向いている人
- 個人や小さなチームで、Web サービスを企画から運用まで回したい
- ログイン後の画面が中心で、SEO が必要なのはトップページくらい
- サーバーの面倒を見たくない、月額の固定費を増やしたくない
同じような構成で Web サービスを作りたい方は、 Web・SaaS 開発 のページもご覧ください。Finoko そのものは finoko.pages.dev で使えます。
About the author
namespace(ネームスペース)
フリーランスの IT コンサルタント兼エンジニア。基幹システム刷新・データ基盤・FA から、Web / SaaS 開発、業務自動化、AI 開発まで一気通貫で支援しています。 相談する →
More posts