TL;DR
どちらも新しいテーブルはデフォルトで閉じています。Appwriteはゲストにもサインイン済みユーザーにも401を返し、SupabaseはテーブルをAPIロールにgrantするまで401/403を返します。違いが出るのは、ルールが間違っているときや、リクエストがユーザーをまたぐときです。Supabaseでは、他のユーザーの行へのupdateが200 []を返し、緩いinsertポリシーのせいでユーザーが別の会社に参加できました。Appwriteでは、私のテストでユーザーをまたぐ試みはすべて401か404で終わり、ユーザーは自分の行を、自分自身か全員以外の誰にも付与できませんでした。Appwriteのサーバーキーにはscopeがあり、Supabaseのsecret keyはすべてのポリシーを素通りします。
Denysです。フルスタック・AIエンジニアで、私が作る、あるいは直すアプリのほとんどはSupabaseで動いています。バックエンドをまだ選んでいるクライアントからは、Appwriteはセキュリティ面でどうなのかとよく聞かれます。機能の一覧では答えが出ないので、両方に同じリクエストを送りました。
テスト方法。 Appwrite 2.3.0を2026年10月30日にDockerでセルフホストし、REST API経由で、ゲスト、サインイン済みの2人のユーザーAとB、2つのサーバーキーとして実行しました。Supabase側は、2026年10月24日にローカルスタック(CLI 2.109.1、Postgres 17.6)で同じロールを使って行った私のテストで、詳細はSupabase RLSポリシー例にあります。どちらも架空の行です。
AppwriteとSupabaseはデータをどう守る?
SupabaseはPostgresでテーブルを守ります。grantがAPIロールがそもそもテーブルに届くかを決め、SQLで書くrow level securityポリシーが、各リクエストが見られる行と書き込める行を決めます。Appwriteは権限をデータの隣に持ちます。テーブルには、読み取り、作成、更新、削除を許可するロールのリストがあり、row securityをオンにすると各行が自分のリストを持ちます。Appwriteの権限のドキュメントでは、アクセスは付与されるだけで拒否されることはなく、ユーザーにはテーブルレベルか行レベルのどちらかで必要で、両方は要りません。
つまり、問題はどちらにアクセス制御があるかではありません。両方にあります。問われるのは、それぞれがどんな間違いを許してしまうかです。
新しいテーブルをそれぞれどう扱う?
| リクエスト | Appwrite、権限の設定なし | Supabase、RLSオン、grantなし(10月24日) |
|---|---|---|
| ゲストが行を一覧する | 401 user_unauthorized |
401 permission denied |
| サインイン済みユーザーが行を一覧 | 401 user_unauthorized |
403 permission denied |
| サインイン済みユーザーが行を追加 | 401 No permissions provided for action ‘create’ |
未テスト |
作ったばかりのテーブルは、どちらでも閉じています。Supabaseではこれは新しい挙動です。2026年10月30日以降に作られたテーブルには、Data APIのgrantが自動では付かなくなりました。これはSupabase 10月30日のgrant変更:壊れる点と確認方法で扱いました。
テーブルが全員に開いているとどうなる?
2つの「開いた」設定は同じものではありません。Appwriteでは、テーブルにread("any")という権限を1つだけ与えました。Supabaseで最もよくある開いた状態は、RLSがオフで、デフォルトのgrantが残っている状態です。
| ゲストのリクエスト | Appwrite、read("any") |
Supabase、RLSオフ、デフォルトgrant(10月24日) |
|---|---|---|
| 行を一覧 | 200、2行とも |
200、2行とも |
| 行を削除 | 401 |
200、2行とも削除された |
Appwriteの「公開」は、書き込みも付与しない限り読み取り専用です。RLSがオフのSupabaseテーブルは、publishable keyを持つ誰でも読み書きできます。だから「RLS disabled in public」が最初に確認する項目なのです。
「自分の行」はそれぞれどう動く?
Appwriteでは、row securityをオンにして、テーブルにcreate("users")という権限を1つだけ与えました。Aが権限を指定せずに行を作ると、Appwriteが自分で権限を設定しました。ユーザーAだけのread、update、deleteです。Supabaseで同じ意図を実現するには、(select auth.uid()) = user_idに対するポリシーが4つ必要です。
| リクエスト | Appwrite(10月30日) | Supabase(10月24日) |
|---|---|---|
| ゲストが一覧 | 200、0行 |
401(anonにgrantなし) |
| Aが行を一覧 | 200、Aの2行 |
200、Aの2行 |
| AがBの行を読む | 404 row_not_found |
一覧には返らない |
| AがBの行をupdate | 401 |
200 [] |
| AがBの行をdelete | 401 |
200 [] |
どちらもBの行は漏らしませんでした。違いは応答です。Supabaseはポリシーが隠す行を除外するので、何にも一致しなかったupdateは、ゼロ行の成功になります。「エラーなし」を「保存できた」とみなすアプリは、編集できたとユーザーに伝えます。Appwriteは、アプリが見逃せないエラーで拒否します。
ユーザーは自分のデータを他人に渡せる?
生成されたコードが最も間違えやすいところなので、両方で試しました。
| Aのリクエスト | Appwrite | Supabase(10月24日) |
|---|---|---|
| ユーザーBが読める行を作る | 401, Permissions must be one of: any, users, user:A… |
ポリシー次第 |
| 自分の行をBの所有に変更 | 401、同じメッセージ |
完全なwith checkなら403、with check (true)なら200 |
| 全員が読める行を作る | 201、その後ゲストが一覧できた |
ポリシーが許す場合のみ |
Appwriteは、ユーザーが付与できるものを、自分自身が持つロール(any、users、自分のユーザー、自分のチーム)に限定します。これにより、私がルールを書かなくても「自分の行を他人に渡す」操作は止まりました。ただし、Aがread("any")で自分の行を全員に公開することは止められませんでした。行を絶対に公開してはいけないなら、権限を設定するのをクライアントにさせず、サーバー関数から作成してください。
Supabaseでは、結果はポリシーの書き方しだいです。完全なwith checkは移動を403で拒否しました。一部のジェネレーターが書くwith check (true)では、AがBのアカウントへ行を移せました。
ユーザーは他人の会社に参加できる?
Supabaseでは、チームのアクセスは通常membershipsテーブルとポリシーで実現します。私のテストでは、多くのチュートリアルが勧めるinsertポリシーwith check (user_id = auth.uid())のせいで、Aは自分を会社2に追加して201 Createdを得て、その給与プロジェクトを読めました。
Appwriteにはチーム機能が組み込まれています。Bがチームを作り、Aはメンバーとして、そしてオーナーとして自分を追加しようとしました。どちらのリクエストも404 team_not_foundを返しました。Aはチームを見ることすらできなかったのです。チームへの所属はサーバーが決めるもので、あなたが書くルールではないため、この漏洩は起こりえません。ただし、行を共有するにはread("team:…")権限が必要で、それは自分で設定します。
サーバーキーは何を開く?
| キー | Appwrite | Supabase(anon vs service roleのテストより) |
|---|---|---|
| フルのサーバーキー | 200、両ユーザーの4行すべて |
3行すべて、RLSを素通り |
狭いscopeのキー(users.read) |
401, missing scope rows.read |
scope付きキーはない |
Supabaseのsecret keyはservice_roleに対応し、BYPASSRLSを持ちます。何が開くかはanon keyとservice role keyの違いにあります。Appwriteのキーも行の権限を無視しますが、作成時のscopeに限られるため、ユーザーを管理する処理用のキーでは、あなたのテーブルは読めません。どちらのキーも、ブラウザのバンドルに入れば、届く範囲すべてが漏れます。
Appwriteの小さな発見が2つあります。テーブルの一覧を求めたゲストとサインイン済みユーザーは、どちらも401でした(この呼び出しにはtables.readが必要で、それを持つのはキーだけです)。そしてread("users")は、サインイン済みのあらゆるアカウントを意味します。サインアップが開いているSupabaseでのauth.uid() is not nullと同じ落とし穴です。
どちらが安全?
どちらもデフォルトで安全とも危険とも言えません。どちらも閉じた状態から始まりました。違いは、間違いが潜む場所です。
| 質問 | Appwrite | Supabase |
|---|---|---|
| アクセスルールの置き場所 | テーブルと行のロールのリスト | SQLのgrantとポリシー |
| 拒否されたユーザー間の書き込み | エラー(401 / 404) |
updateとdeleteは0行の200 |
| 行を他者に付与 | ユーザーが持つロールにのみ | ポリシーが許す範囲 |
| 列の値、MFAレベルに基づくルール | 権限にはない。サーバーコードが必要 | ポリシーで可能。例:機密行にaal2 |
| サーバーキー | scope付き | secret keyはすべてのポリシーを素通り |
Appwriteのモデルのほうが狭く、私のテストでは、その狭いモデルが、何も書かなくてもユーザー間のあらゆる手口を拒否しました。SupabaseのモデルはSQLなので、Appwriteの権限では表せないルール、たとえば「MFAのときだけ」や「自分が所属する会社の行だけ」を表現できますが、間違ったルールも表現できてしまいます。チームがポリシーを読まないなら、Appwriteのデフォルトのほうが許容範囲が広いです。データに依存するルールが必要なら、Supabaseでは書けますが、そのあとテストが必要です。私の方法はSupabase RLSのテスト方法にあります。
Supabaseプロジェクトをどう確認する?
無料のSupabaseセキュリティチェックは、プロジェクトを読み取り専用で読み、RLSがオフのテーブル、常に真になるポリシー、ゲスト向けの書き込みポリシー、ポリシーが示す以上の権限を与えるgrantを指摘します。Supabaseが守ってくれる範囲と、あなたに任される範囲はSupabaseは安全?にあります。Appwriteに相当するものはありません。Appwriteでは、上のリクエストを2人のユーザーとして実行してください。Firebaseについては、同じ種類の攻撃をFirebaseセキュリティルールの例で試しました。
制限。 ノートPC上のセルフホストのAppwrite 2.3.0を1つ、ローカルのSupabaseスタックを1つ、6日の間隔で、架空の行で試しました。Appwrite Cloudはセルフホストと異なる可能性があります。Storage、関数、realtime、認証設定、コンプライアンス認証は、ここでは比較していません。