個人開発でいちばん時間を溶かすのは、アプリ本体ではない部分だったりします。認証、データベース、サーバー、課金レシートの検証、計測 — どれも「作りたかったもの」ではないのに、無いとリリースできない。

それを丸ごと借りるのがBaaS(Backend as a Service)です。僕は現在3つのプロダクトを動かしていて、その構成はこうなっています。

プロダクト 種別 バックエンド
花庭 iOSアプリ(AI画像生成) Firebase一式
Morphica iOSアプリ(AI画像生成) Supabase + Cloudflare Workers
AppVillage Webサービス Supabase

AI画像生成のiOSアプリが2本あって、片方はFirebase、片方はSupabaseです。この記事では、その構成の実物と、なぜ分かれたのかを正直に書きます。

この記事で分かること

  • BaaSとは何か、具体的に「何を作らなくてよくなる」のか
  • 同じ要件(AIのキー保護・チケット課金)を、FirebaseとSupabaseで解いた2通りの実装
  • Firebaseにしかできないこと、Supabaseでも代替できること
  • 選定を実際に動かしたのは技術要件ではなかったという話

結論: 技術要件で選んだつもりが、実は「無料枠の単位」で決まっていた

先に結論を書きます。

  • BaaSの本質は「作らなくていいものリスト」。個人開発では、作らない判断そのものが速度になる
  • FirebaseとSupabaseは、どちらでも大抵のものは作れる。差が出るのは端末認証まわりで、Firebaseは設定するだけ、Supabase構成ではAppleのAPIを自分で叩くことになる
  • そして実際の決め手は、無料枠の課金単位でした。Supabaseはプロジェクト数(無料は2つまで)、Firebaseは使用量。プロダクトを複数持つ個人開発者には、これが技術的な優劣より効く
  • ロックインは「差し替え点を1つ作っておく」だけで、実務上ほぼ怖くなくなる

そもそもBaaSとは

バックエンドの部品一式をサービスとして借りる仕組みです。自前でやる場合との差はこうなります。

必要なもの 自前でやると BaaSなら
認証 OAuth/トークン管理を実装・運用 SDKを数行
データベース サーバー用意・バックアップ・スケール マネージド
ファイル保存 ストレージ・CDN・権限設計 標準機能
サーバー処理 サーバー常駐・デプロイ・監視 サーバーレス関数
計測 ログ基盤 標準の分析ツール

個人開発における選択肢は実質、Firebase(Google)とSupabase(OSS・PostgreSQLベース)の二強です。以下、両方の実物を見ていきます。

ケース①: 花庭 = Firebase一式

花庭は、撮った花の写真をAIで水彩イラストに変換して庭に植えるアプリです。もともとは完全ローカル(端末内のSQLiteだけ)で設計していました。バックエンドは要らないはずでした。

きっかけ: AIを入れた瞬間、バックエンドが必須になった

画像生成AIを入れた時点で、設計が崩れます。APIキーをアプリに載せられないからです。

Flutterでビルド時に埋め込む--dart-define=GEMINI_API_KEY=...は、IPAの中に平文で焼かれますstringsコマンド1発で抜けます。課金の有無に関係なく、これはリリースブロッカーです。

つまり「キーをアプリに置かない」=どこかにサーバーが要る。ここでBaaSの出番でした。採用したのはFirebase AI Logicで、これは一言でいうと「サーバーコードを書かずにAPIキーを隠せる関所」です。アプリはFirebase SDK経由でモデルを呼び、キーはGoogle側に置かれたままアプリには載りません。

App Check: ログイン画面ゼロで「うちのアプリからの呼び出し」だけ通す

関所を作っても、そのエンドポイントを誰でも叩けたら意味がありません。ここでApp Check(iOSでは App Attest)を使いました。端末とアプリの正当性をOSレベルで証明する仕組みで、これをENFORCE(適用)にすると、正規のアプリ以外からの呼び出しが遮断されます。

重要なのは、これはログインではないということです。ユーザーにアカウント登録を求めずに、キーの不正利用だけを止められる。「ログイン必須にすると離脱するけどキーは守りたい」という場面に、これがきれいに刺さります。

Sign in with Apple: 「認証のため」ではなく「残高の持ち主」を決めるため

一方で、ログインも結局入れました。理由はセキュリティではなく課金です。

花庭のAI生成は消費型のチケット制(無料お試し5回 → 以降はチケット購入)にしました。すると「残高が誰のものか」を機種変や再インストールをまたいで決める必要が出ます。端末内のカウンタは再インストールでリセットされる=簡単に突破されるので、恒久的なIDが要る。それがSign in with Appleを入れた理由です。

認証が必要だからログインを付ける、ではなく、持ち主を決める必要があるからログインを付ける。この順番で考えると、入れる/入れないの判断が明確になります。

Firestore + Cloud Functions: 残高は「サーバーが真」

チケット残高は Firestore の users/{uid} に持たせ、クライアントからの書き込みを全面禁止にしました。セキュリティルールはこれだけです。

// firestore.rules — 残高は Cloud Functions(Admin SDK)経由でしか書けない
match /users/{uid} {
  allow read: if request.auth != null && request.auth.uid == uid;
  allow write: if false;   // クライアントからの書き込みは一切許可しない
}

書き込みはCloud Functions 4本(東京リージョン)だけが行います。

関数 役割
getBalance 残高取得。無ければ無料お試し5回つきで台帳を作成(冪等)
consumeTicket 生成1回ぶんを消費。無料枠 → チケットの順に減らす
refundTicket 生成が失敗したら消費ぶんを払い戻す
revenuecatWebhook 購入イベントを受けて残高を加算する

すべてFirestoreのトランザクション内で読み書きし、enforceAppCheck: true を付けています。「残高はサーバーが真、クライアントの申告は信じない」を設計の一行目に置くと、あとの実装が全部素直になります。

RevenueCat: レシート検証も借りる

課金まわりで自前実装がいちばん怖いのがレシート検証(署名の検証、返金の巻き戻し、再送への対応)です。ここはRevenueCatというサービスに委譲しました。検証はRC側で済ませ、うちは購入イベントのWebhookを受けて残高を足すだけにしています。

ただしWebhookには落とし穴があって、この手の配信はat-least-once(同じイベントが2回届きうる)です。素直に書くとチケットが二重に付与されます。対策は、イベントIDを購入記録のドキュメントIDにして冪等化することでした。

// 再送されても二重加算しないよう、RCの event.id をそのままドキュメントIDにする
const purchaseRef = userRef.collection('purchases').doc(eventId)

await db.runTransaction(async (tx) => {
  if ((await tx.get(purchaseRef)).exists) return false   // 再送 → 何もしない
  tx.set(purchaseRef, { productId, tickets: count, createdAt: FieldValue.serverTimestamp() })
  tx.set(userRef, { tickets: ledger.tickets + count }, { merge: true })
  return true
})

RevenueCatの料金は、確認した時点(2026年7月)で月間トラッキング収益$2,500まで無料。個人開発の規模ならまず無料枠です。

結果: Firebaseだけで一通り揃った

花庭で最終的にFirebaseが担っているのは、こうなりました。

  • AI Logic — APIキーの関所
  • App Check — 端末認証(ENFORCE)
  • Auth — Sign in with Apple(残高の持ち主)
  • Firestore — チケット台帳(サーバー権威)
  • Cloud Functions — 消費・払い戻し・購入Webhook
  • Analytics(GA4) — イベント計測
  • Hosting — プライバシーポリシー・利用規約・特商法の3ページ

最後のHostingは地味ですが効きます。iOS審査ではこれらのURL提出が必要で、そのためだけにサーバーを借りるのは馬鹿らしい。Firebaseを使っているなら静的ページを無料で置けて、そのURLをそのまま提出できます。

そして重要なのは、ユーザーのデータ本体は端末内のSQLiteのままだということ。BaaSは「全部載せる」必要はなく、守りたいものだけを預ければいい、という判断です。

ハマったところ(Firebase編)

  • シミュレータでは App Attest が動かない → 開発中ずっと生成が401。デバッグプロバイダのトークンをFirebaseコンソールに登録して解決。これを知らないと「実装が悪いのでは」と延々疑うことになります
  • SDKのバージョンで機能が無いfirebase_ai 2.x は image-to-image 未対応でUnimplementedError。3.12.2以上が必要でした
  • モデルIDを変えたら背景色が変わった → 生成画像の背景をクロマキーで透過する処理が効かなくなり、モデル選定をやり直し。「モデルを新しくすれば良い」とは限らない
  • Blazeプラン(従量課金)への切り替えが必須 → AI Logicを使うには無料プランのままでは始められません。予算アラート(僕は月¥3,000)は必ず設定

ケース②: Morphica = Supabase + Cloudflare Workers

ここが記事の肝です。Morphicaも同じくAI画像生成のiOSアプリで、キー保護もチケット課金も要件はほぼ同じ。でも構成はまったく違います。

  • Supabase — 認証・DB・ストレージ
  • Cloudflare Workers — AI APIのプロキシ(キー保護)
  • Nano Banana API(Gemini 3 Pro Image) — 画像生成

キーの守り方が対照的です。花庭はFirebase AI Logicという既製の関所を借りましたが、Morphicaは自分で関所を建てています。

アプリ → Cloudflare Workers → Nano Banana API
          ↑ APIキーはここ(Workersのシークレット)
          ↑ SupabaseのJWTを検証してから通す
          ↑ レート制限は Workers KV でカウント

WorkersがSupabaseのJWTを検証し、正規のログインユーザーからの呼び出しだけをAIへ通す。つまり花庭の「App Check(端末認証)」の位置に、Morphicaでは「Supabaseのログイン+Workers側のレート制限」が入っています。

Appleの「端末API」は2種類ある — 守っているものが違う

ここで整理しておきたいことがあります。Morphicaも不正防止にAppleの端末APIを使っているのですが、花庭とは目的が違うのです。

App Attest(Firebase App Check) DeviceCheck の永続ビット
何を証明するか 呼び出し元が本物のアプリ その端末が過去に何をしたか(2ビット)
守っているもの APIキー・エンドポイント 無料枠の再取得(アカウント作り直し)
再インストール しても消えない
花庭 ✅ ENFORCEで適用
Morphica ✅ iOS。AndroidはPlay Integrity

Morphicaの実装はこうです。新規ユーザーのサインイン直後にWorkersの/v1/device-checkへ問い合わせ、この端末が過去に無料チケットを受け取っているかを確認する。受け取り済みなら残数を0にし、初めての端末なら「登録済み」としてビットを立てる。AppleのDeviceCheck APIは秘密鍵で署名したJWTが必要なので、その処理は当然すべてWorkers側です。

同じ「Appleの端末API」でも、片方はキーを守り、片方は無料枠を守る。混同しやすいところですが、必要になる場面はまったく別です。無料トライアルを配るアプリなら後者はほぼ必須で、これが無いとアカウントを作り直すだけで無料枠が無限に手に入ってしまいます。

そしてもう1つ、この構成にはAI Logicに無い自由があります。Firebase AI LogicはGoogleのモデル(Gemini/Imagen)専用ですが、自前プロキシならモデルもベンダーも自由に差し替えられます。OpenAIやClaudeを使う予定があるなら、Firebase AI Logicの優位はそもそも無いということです。

ケース③: AppVillage = Supabase

Webサービスであるこのサイトは、素直にSupabaseです。

  1. 行単位の権限がそのまま仕様になる — 「誰が・どの行を・読めるのか/書けるのか」がWebでは要件の中心です。SupabaseはPostgreSQLのRLS(Row Level Security)でこれをDB側に書けます。アプリのif文ではなく、DBの門番に守らせる形にできる
  2. 検索・並び替え・集計がSQLで書ける — アプリ一覧、タグ絞り込み、新着順。この手の要件はリレーショナルDBが素直です
  3. Next.jsとの相性 — サーバーコンポーネントから直接叩ける。ただしここには罠が1つあって、それで本番のTTFBが3秒になった事件はSupabaseの実例記事に詳しく書きました

Supabaseでも同じことはできるのか?

花庭でやったことを、Supabaseだけで再現できるか。調べ直した結論はこうです。

花庭でのFirebaseの役割 Supabaseでの代替 判定
AI Logic(キーの関所) Edge Functions + シークレット(自前プロキシ) ○ 同等。ただし自分で書く
App Check(端末認証) 純正機能なし。Edge Functions/WorkersからAppleのAPIを自分で叩く 設定で済むのはFirebaseだけ
Auth(Sign in with Apple) signInWithIdToken() でネイティブ対応 ◎ 同等以上
Firestore + Functions(台帳) Postgres + RLS + Edge Function ◎ むしろ書きやすい

キーの関所は、Edge Functionにキーをシークレットとして持たせれば同じことができます。差は「サーバーコードを書くかどうか」だけ。Morphicaが実際にCloudflare Workersでやっているのと同じ構図です。

App Check相当が唯一の非対称です。Supabaseに端末認証の純正機能はないので、Edge FunctionsやWorkersからAppleのAPIを自分で叩くことになります。DeviceCheckの永続ビットならMorphicaのように現実的な実装量ですが、App Attest(アプリ正当性の証明)まで自前でやるとCBORのデコード、ルート証明書までのX.509チェーン検証、nonce照合、署名検証とカウンタ管理が必要で、これはかなり重い。Firebaseならコンソールで登録してENFORCEにするだけなので、ここだけは労力差がはっきり出ます。

Sign in with AppleはむしろSupabaseが楽で、ネイティブのみの実装ならApple側のシークレットキーを6ヶ月ごとに更新する作業が不要です(Web OAuthフローだと必要)。

台帳もPostgresの方が素直に書けます。「クライアントから書けない」はRLSでポリシーを1本も書かないだけで成立し(ポリシーが無い=アクセス不可)、Webhookの冪等化はevent_idUNIQUE制約を張るだけ。残高の減算もUPDATE ... WHERE tickets > 0 RETURNINGで1文アトミックです。

つまり技術的には、App Check以外はどちらでも作れる。ではなぜ分かれたのか。

正直な話: 決め手は「無料枠の単位」だった

ここまで技術的な理由を並べてきましたが、花庭でFirebaseを選んだ本当の理由を書きます。

Supabaseのプロジェクト数を増やしたくなかったからです。

Supabaseの無料枠はプロジェクト2つまで。当時すでにMorphicaとAppVillageで埋まっていて、花庭で3つ目を作れば月$25のProプランに乗ります。まだ1ダウンロードもされていないアプリのために月$25は、個人開発では重い判断です。

一方Firebaseは、プロジェクトをいくつ作っても課金は使用量。使わなければ無料のまま増やせます(AI Logicを使うにはBlazeプランへの切り替えが要りますが、これは「従量課金を有効にする」だけで固定費ではありません)。

ここに、個人開発でわりと見落とされている論点があります。

BaaSの無料枠が「プロジェクト単位」か「使用量単位」かで、複数プロダクトを持つ人の設計判断は変わる。

1本しか作らない人には関係のない話ですが、2本目・3本目を出す個人開発者にとっては、技術的な優劣よりこちらが先に効きます。「プロダクトが増えるほど、同じBaaSに寄せづらくなる」という力学が働くわけです。

(なお、その後AppVillage側の事情でSupabaseはProプランになりました。詳しくは外部サービス構成の記事に書いています)

判断表: どちらに寄せるか

3本を作った現時点での、正直な判断基準です。

こういう条件なら 寄せ先
Supabaseの無料プロジェクト枠(2つ)が埋まっている Firebase(または自前プロキシ)
端末認証をコードを書かずに入れたい Firebase(App Check)
プッシュ通知・クラッシュ計測もまとめたい Firebase
使うAIモデルがGeminiで確定している Firebase(AI Logic)
Google以外のAIモデルも使うかもしれない Supabase + 自前プロキシ
検索・絞り込み・集計が機能の中心 Supabase
行単位の権限がそのまま仕様 Supabase(RLS)
SQLが書ける・書きたい Supabase
いつか自前サーバーへ移すかもしれない Supabase(標準のPostgreSQL)

「アプリだからFirebase、WebだからSupabase」ではありません。同じiOSアプリでも、条件が変われば答えは変わります — その実例が花庭とMorphicaです。

費用のリアル

  • 花庭 / Firebase — Blazeプラン(従量課金)への切り替えは必須ですが、請求は無料枠内に収まっています。正直に言えば、ユーザーが少ないからです。伸びた場合はFirebase本体より先にAI生成の従量コストが効いてくる設計なので、そこは価格側(チケット制)で吸収します
  • RevenueCat — 月間トラッキング収益$2,500まで無料
  • Supabase — Proプラン$25/月。ただし課金理由は性能ではなくプロジェクト数(外部サービス構成の記事)
  • Cloudflare Workers — 無料枠が十分に広く、個人開発規模ではまず超えません

結論として、バックエンドの固定費は事実上ゼロから始められる。個人開発でBaaSを使わない理由が無いのは、この一点が大きいです。

ロックイン対策: 差し替え点を1つ作る

BaaSの唯一にして最大のデメリットがベンダーロックインです。対策として僕がやっているのは、大げさな抽象化ではなく「差し替え点(seam)を1つだけ作る」ことです。

花庭では、AI画像生成の呼び出し口をインターフェース1つの裏に置いています。

/// 写真をイラストに変換するAI画像クライアント。
/// アプリ側はこの型しか知らない = 裏の実装は差し替え可能。
abstract interface class AiImageClient {
  Future<Uint8List?> illustrate(Uint8List photo, String prompt);
  bool get isEnabled;
}

実際、開発中に「APIキー直叩きの実装」→「Firebase AI Logicの実装」へ乗り換えましたが、変更したのは実装クラス1つと、DIしているプロバイダの1行だけでした。同じ手順で、MorphicaがやっているようなWorkers経由やSupabase Edge Functionへも移れます。

原則はシンプルで、BaaS固有の型をアプリ全体に漏らさないこと。境界を1箇所に集めておけば、乗り換えコストは「その1ファイルを書き直す」まで下がります。ロックインが怖いからBaaSを避ける、というのは個人開発では損な判断です。

BaaSを使う前に知っておくこと

メリットは十分書いたので、正直なデメリットも。

  • ロックイン — 上記の通り、境界設計で緩和する
  • 料金が使用量連動で読みにくい — 予算アラートは初日に設定する。Vercelの画像最適化クォータを使い切って本番の画像が全部消えた話は、この油断の実例です
  • 設計が「そのBaaSの流儀」に引っ張られる — Firestoreのクエリ制約に合わせてデータ構造を決める、といったことが起きます。要件が先か、DBの都合が先か、迷ったら要件を優先する
  • 無料枠は変わりうる — 事業判断で変更されます。乗り換えられる状態を保っておくことが保険になります

まとめ

  • BaaSは「作らなくていいものリスト」。個人開発では、作らない判断が速度そのものになる
  • 同じAI画像生成iOSアプリでも、花庭はFirebase一式、MorphicaはSupabase+Cloudflare Workers。同じ要件は複数の解き方で満たせる
  • 技術的に差が出るのは端末認証まわりだけ。Firebaseは設定するだけで済み、Supabase構成ではAppleのAPIを自分で叩く(Morphicaは実際そうしている)
  • 実際の決め手は無料枠の単位だった。Supabaseはプロジェクト数、Firebaseは使用量。複数プロダクトを持つほど、この違いが技術要件より効く
  • ロックイン対策は差し替え点を1つ作ること。境界を1ファイルに集めれば、乗り換えは怖くない

バックエンドは「作れるか」ではなく「作らずに済ませられるか」で考える。そして選定理由は、技術記事に書かれる建前より、たいていもっと生活に近いところにあります。