Flutter アプリの AI を VPS プロキシ経由にした理由と実装
API キーをクライアントに置かない。Express プロキシ、認証、ゲスト用の分離セッション。
Flutter アプリに AI を載せるとき、ついクライアントから API を直呼びしたくなります。Angla ではそれをせず、VPS 上の Node(Express)プロキシ経由で Gemini を呼ぶ形にしています。
理由は単純で、キーがアプリに入ると配布物から抜き出されるリスクがあるからです。
クライアント直呼びがつらい理由
モバイルや Web のクライアントは、最終的にユーザーの手元で動きます。難読化しても、API キーを完全に隠せません。レート制限や課金の制御も、クライアント任せだと壊れやすいです。
なので Angla では、アプリは自前 API だけを叩き、Gemini への接続情報はサーバー側の環境変数に置きます。
構成
流れはこうです。
- アプリが釣場の天気などを集める
- VPS のプロキシ(例:
/api/fishing-advice)へ送る - サーバーが Gemini を呼び、結果を返す
プロバイダ実装はサーバー側に閉じているので、あとからモデルを差し替えてもアプリの配布物を急ぎ書き換えなくて済みます。実際、本番は Groq から Gemini へ移しています(旧実装はロールバック用に残置)。
認証とレート制限
プロキシは「誰でも無制限」にはしません。Supabase のユーザー JWT を検証し、そのうえで呼び出し回数を抑えます。成功したときだけ消費する、といった細部も運用しながら直しています。
アプリ側に Gemini キーを配らない、が第一条件。そのうえで、サーバー側で滥用を止める、という二段です。
ゲスト利用を分けた話
未ログインでも AI を少し試せるようにしたくて、ゲスト向けの経路を追加しました。ただし、いつものログイン状態(お気に入り同期など)と混ぜると危ないので、AI 用に分離した Supabase クライアントで匿名サインインし、トークンだけプロキシに渡す形にしています。
ポイントは次です。
- メインのログイン状態は汚さない
- 分離クライアントはトークン取得以外に使わない(他テーブルを直接叩かない)
- ゲストは消費を抑えるため、単日フォーマット側に寄せる、など運用上の制限を付ける
「便利だから匿名認証を足す」だけでなく、セッションの境界を先に決めたのが効いています。
公開時に気をつけていること
- 環境変数名だけを文書化し、値は出さない
- ログにリクエスト全文やキーを残さない
- 無料枠と有料フォールバックがある場合でも、キーを用途別に分ける
AI の話はモデル名より、鍵と境界の設計の方が長く残ります。