はじめに
Supabaseで個人開発をしていると、想像以上に早くアプリの形になります。
認証もある。DBもある。Storageもある。フロントからそのまま触れて、数日で「それっぽいもの」ができる。このスピード感は本当に魅力です。
ただ、その速さゆえに、あとから振り返ると「動いてはいたけど、あれは普通に危なかったな」と思う設計が結構あります。
特に個人開発では、まず作ってみることが正義になりやすいので、セキュリティや責務分離が後回しになりがちです。
Supabaseの公式ドキュメントでも、anon や publishable key で使う場合は、RLSを有効にし、権限を継続的に見直すことが前提だとされています。
今回は、私自身もやりがちだと感じる「動いてるけど危ない設計」を7つに絞って整理します。
早い段階で意識しておくと、あとで効いてきます。
1. service_role をアプリ側で雑に使う
これは一番危ないです。しかも、最初は便利すぎて危険性に気づきにくいです。
たとえば「RLSで詰まったから、とりあえず service_role を使えば全部動く」という発想です。
確かに動きます。でもそれは、設計が正しいから動いているのではなく、強い鍵で全部開けているだけです。
Supabase公式でも、service_role と secret key は開発者が管理する安全なサーバー側でのみ使うべきで、service_role は BYPASSRLS 属性を持ち、RLSをすべて迂回すると明記されています。ブラウザで使うことも禁止されています。
個人開発でやりがちなのは、フロント側の .env に入れてしまうことです。ローカルでは動きますし、実装も楽です。
でも、それは「認可設計を考えなくて済んでいる」のではなく、「認可を飛ばしている」状態です。
service_role は、管理画面、バッチ、集計処理、サーバーサイドの厳格な内部処理など、明確に限定された場所でだけ使うべきです。
通常のアプリ利用者が触る画面やクライアント処理では、anon / publishable key と RLS で成立する設計に寄せるべきです。
2. 「ログインしてるから安全」と思っている
これはRLSの話と地続きですが、かなり多い勘違いです。
Supabase Authでログイン機能をつけると、急にアプリがちゃんとして見えるようになります。
ですが、認証と認可は別物です。
ログインできることと、そのユーザーがどのデータを読めて、どの行を更新できるかは、別で設計しないといけません。
SupabaseのAuthドキュメントでも、authentication は本人確認、authorization はその人に何が許可されるか、という別の責務として整理されています。
よくあるのは、「ログイン済みなら全件取得OK」になっている状態です。
たとえば posts テーブルに対して、authenticated なら select を許す、みたいなポリシーです。これだと、誰でも全ユーザーのデータを見られます。
個人開発の初期は「まだユーザー少ないし」で流しがちですが、その設計のまま機能が増えると、途中から直すのがかなりしんどくなります。
大事なのは、「ログインしているか」ではなく「その行にアクセスしていい本人か」で考えることです。
3. profiles を雑に作る、あるいは auth.users の扱いを曖昧にする
Supabaseを触り始めると、ユーザー情報をどこに持つかで一度は迷います。
ここで危ないのは、auth.users をそのままアプリのユーザー情報テーブルのように扱おうとすることです。
Supabase公式では、Auth schema は自動生成APIに公開されておらず、API経由で扱いたいユーザー情報は public スキーマに自前テーブルを作る形が案内されています。さらに public.profiles のようなテーブルを作り、auth.users を参照しつつ on delete cascade を指定する例も示されています。
この設計を曖昧にすると、あとでだいたい壊れます。
たとえば、profiles.id と auth.users.id が一致しない、削除時に関連データが残る、users 的な独自テーブルを二重管理してしまう、などです。
個人開発では、「名前・アイコン・自己紹介」みたいなアプリ用プロフィール情報は public.profiles に寄せ、認証基盤としてのユーザー管理は auth.users に寄せる、という責務分離を最初から意識したほうが後で楽です。
雑に始めても動きますが、データ整合性の観点ではかなり危なめです。
4. insert 時の user_id をクライアント任せにする
これも動いてしまうので怖いポイントです。
たとえば、フロント側で user_id: session.user.id を付けて insert する実装はよくあります。もちろん、それ自体は普通に書けますし、開発中は問題なく見えます。ただ、クライアントが渡す値をそのまま信じる設計は、境界の置き方として弱いです。
本来は、「この行の所有者は誰か」をDB側でも一貫して検証できる形に寄せるべきです。RLSで auth.uid() = user_id を見るなら、少なくとも insert ポリシーまで含めて整合するようにしておかないと、「見える条件」と「作れる条件」がズレます。
RLSが有効なのにデータが返らないケースは、ポリシー未設定や、認証付きリクエストになっていないことがよくあると、Supabaseのトラブルシュートでも説明されています。
個人的には、クライアントで user_id を渡すにしても、「DB側のポリシーで必ず本人しか作れない」「更新も本人だけ」「削除も本人だけ」までセットで考えるべきだと思っています。
値を渡しているだけで安全が担保されるわけではありません。
5. Storage をファイル置き場としてだけ見ている
Storageも危ないです。画像アップロードが動いた瞬間に安心しやすいのですが、ここもちゃんと認可設計が必要です。
Supabase Storage は Postgres のRLSと連携していて、アップロード可否などは storage.objects に対するポリシーで制御します。
しかも、デフォルトではRLSポリシーなしに自由アップロードできるわけではなく、必要な操作を明示的に許可する設計です。
つまり、DBだけRLSを頑張って、Storageはバケット作って終わり、では不十分です。
プロフィール画像、投稿画像、添付ファイルなど、どのパスに、誰が、何を、どこまで操作できるのかを考えないと、意図しない参照や上書きの温床になります。
個人開発だと「画像だからまあいいか」となりがちですが、ファイルはむしろ漏れると分かりやすく事故になります。DBの行単位だけでなく、Storageのオブジェクト単位でも権限設計が必要です。
6. RLSをセキュリティ機能としてしか見ておらず、性能を無視する
RLSはセキュリティの話ですが、実運用では性能とも直結します。
Supabaseのベストプラクティスでも、RLSで使う列にインデックスを張ること、auth.uid() や auth.jwt() を毎行評価するような書き方を避ける工夫が示されています。
特に、RLS条件に使う user_id のような列にインデックスがないと、大きなテーブルで急に重くなります。公式のトラブルシュートでも、該当列へのインデックスで大きな改善が見られるケースが紹介されています。
個人開発の初期はデータ件数が少ないので見えません。
でも、投稿、いいね、コメント、学習記録、ワークアウトログのように件数が積み上がるサービスでは、後からじわじわ効いてきます。
危ないのは、「安全だからOK」で終わることです。正しくは、「安全で、かつ現実的な速度で動くか」を見ないといけません。RLSは書いて終わりではなく、テーブル設計やインデックス設計とセットです。
7. RLSを絞り込みの代わりにしている
これも地味ですが、かなり大事です。
たとえば「RLSで auth.uid() = user_id にしてるから、クエリでは全件 select() でいいでしょ」と考えるパターンです。
もちろんセキュリティ上はそれで守れるかもしれません。
ただ、SupabaseのRLSベストプラクティスでは、RLSはあくまでセキュリティ境界として使い、必要なフィルタはクエリ側でも書くことが推奨されています。
.eq('user_id', userId) のような条件を明示したほうが、意図もはっきりしますし、性能上も有利です。
これは思想の話でもあります。
RLSは最後の防波堤であって、普段のアプリの問い合わせロジックそのものではありません。
アプリ側はアプリ側で、必要なデータだけを取りに行く。
DB側は、それでも越えてはいけない境界を守る。この二段構えのほうが健全です。
「RLSを書いたからクエリは雑でいい」という状態は、短期的には楽ですが、運用が始まると読みづらさも重さも増えていきます。
おわりに
Supabaseは本当に便利です。個人開発との相性もかなりいいです。
だからこそ、危ない設計でも一度“動いてしまう”のが難しいところだと思います。
特に個人開発では、最初はスピード優先でいい場面もあります。
ただ、そのスピードを活かすためにも、「何を雑にしてよくて、何は最初から外してはいけないか」は知っておいたほうがいいです。service_role をフロントで使わないこと、認証と認可を分けて考えること、profiles と auth.users の責務を分けること、StorageにもRLSが必要だと理解すること。このあたりは、後回しにすると修正コストが一気に跳ね上がります。
前回のRLSの記事が刺さったのは、たぶん「個人開発で見落としやすいけど、本当は大事」というポイントに触れていたからだと思います。今回の話も同じです。動いていることと、安全で拡張しやすいことは、まったく別です。
Supabaseは“楽に作れる”からこそ、“雑に作れてしまう”。そこを越えたあたりから、個人開発の質が一段上がる気がしています。