TL;DR
Beide sperren eine neue Tabelle standardmäßig: Appwrite antwortete einem Gast und einem angemeldeten Nutzer mit 401, Supabase antwortet mit 401/403, bis Sie die Tabelle den API-Rollen freigeben. Der Unterschied zeigt sich, wenn eine Regel falsch ist oder eine Anfrage Nutzergrenzen überschreitet. Bei Supabase lieferte ein Update auf die Zeile eines anderen 200 [], und eine zu lockere Insert-Policy ließ einen Nutzer einer fremden Firma beitreten; bei Appwrite kam jeder nutzerübergreifende Versuch in meinem Lauf als 401 oder 404 zurück, und ein Nutzer konnte seine Zeile nur sich selbst oder allen freigeben. Appwrite-Server-Keys haben Scopes; ein Supabase Secret Key umgeht jede Policy.
Ich bin Denys, Full-Stack- und KI-Entwickler. Die meisten Apps, die ich baue oder repariere, laufen auf Supabase, und Kunden, die noch ein Backend wählen, fragen, wie Appwrite bei der Sicherheit abschneidet. Featurelisten beantworten das nicht, also habe ich beiden dieselben Anfragen geschickt.
So habe ich getestet. Appwrite 2.3.0, selbst gehostet mit Docker am 30. Oktober 2026, über die REST-API als Gast, als zwei angemeldete Nutzer A und B und mit zwei Server-Keys. Die Supabase-Seite ist mein Lauf auf einem lokalen Stack (CLI 2.109.1, Postgres 17.6) vom 24. Oktober 2026 mit denselben Rollen, ausführlich beschrieben in Supabase RLS Policy Beispiele. Auf beiden Seiten ausgedachte Zeilen.
Wie schützen Appwrite und Supabase Daten?
Supabase schützt Tabellen mit Postgres: Ein Grant entscheidet, ob die API-Rollen eine Tabelle überhaupt erreichen, und Row-Level-Security-Policies, in SQL geschrieben, entscheiden, welche Zeilen jede Anfrage sieht und schreibt. Appwrite speichert Berechtigungen neben den Daten: Eine Tabelle hat eine Liste von Rollen, die lesen, anlegen, ändern oder löschen dürfen, und bei eingeschalteter Row Security trägt jede Zeile ihre eigene Liste. In den Appwrite-Berechtigungsdokumenten wird Zugriff nur gewährt, nie verweigert, und ein Nutzer braucht ihn auf Tabellen- oder Zeilenebene, nicht auf beiden.
Die Frage ist also nicht, wer Zugriffskontrolle hat. Beide haben sie. Die Frage ist, wo sich jeweils Fehler einschleichen können.
Was passiert mit einer neuen Tabelle?
| Anfrage | Appwrite, keine Berechtigungen gesetzt | Supabase, RLS an, kein Grant (24. Okt.) |
|---|---|---|
| Gast listet Zeilen | 401 user_unauthorized |
401 permission denied |
| Angemeldeter Nutzer listet Zeilen | 401 user_unauthorized |
403 permission denied |
| Angemeldeter Nutzer fügt Zeile ein | 401 No permissions provided for action ‘create’ |
nicht getestet |
Eine frische Tabelle ist bei beiden geschlossen. Bei Supabase ist das neu: Tabellen, die nach dem 30. Oktober 2026 angelegt werden, bekommen nicht mehr automatisch Data-API-Grants; das habe ich in der Grant-Änderung vom 30. Oktober beschrieben.
Was passiert, wenn eine Tabelle für alle offen ist?
Die beiden „offenen“ Einstellungen sind nicht dasselbe. In Appwrite habe ich der Tabelle eine einzige Berechtigung gegeben, read("any"). Bei Supabase ist der übliche offene Zustand RLS aus, während die Standard-Grants bestehen bleiben.
| Anfrage eines Gastes | Appwrite, read("any") |
Supabase, RLS aus, Standard-Grants (24. Okt.) |
|---|---|---|
| Zeilen listen | 200, beide Zeilen |
200, beide Zeilen |
| Zeile löschen | 401 |
200, beide Zeilen gelöscht |
„Öffentlich“ heißt bei Appwrite nur lesbar, solange Sie nicht auch Schreibrechte vergeben. Eine Supabase-Tabelle mit ausgeschaltetem RLS ist für jeden mit dem Publishable Key les- und schreibbar. Deshalb prüfe ich „RLS disabled in public“ als Erstes.
Wie funktionieren „eigene Zeilen“ jeweils?
In Appwrite habe ich Row Security eingeschaltet und der Tabelle eine Berechtigung gegeben, create("users"). Als A eine Zeile ohne Berechtigungsangabe anlegte, setzte Appwrite sie selbst: read, update und delete nur für Nutzer A. Bei Supabase sind es für dieselbe Absicht vier Policies auf (select auth.uid()) = user_id.
| Anfrage | Appwrite (30. Okt.) | Supabase (24. Okt.) |
|---|---|---|
| Gast listet Zeilen | 200, 0 Zeilen |
401 (kein Grant für anon) |
| A listet Zeilen | 200, As 2 Zeilen |
200, As 2 Zeilen |
| A liest Bs Zeile | 404 row_not_found |
nicht in der Liste enthalten |
| A ändert Bs Zeile | 401 |
200 [] |
| A löscht Bs Zeile | 401 |
200 [] |
Keiner von beiden gab Bs Zeile preis. Der Unterschied ist die Antwort. Supabase filtert Zeilen heraus, die eine Policy verbirgt, daher ist ein Update ohne Treffer ein Erfolg mit null Zeilen; eine App, die „kein Fehler“ als „gespeichert“ versteht, sagt dem Nutzer, seine Änderung habe geklappt. Appwrite lehnt mit einem Fehler ab, den die App nicht übersehen kann.
Kann ein Nutzer seine Daten an jemand anderen übergeben?
Hier geht generierter Code am häufigsten schief, deshalb habe ich es bei beiden versucht.
| Anfrage von A | Appwrite | Supabase (24. Okt.) |
|---|---|---|
| Zeile anlegen, die B lesen darf | 401, Permissions must be one of: any, users, user:A… |
hängt von der Policy ab |
| Eigene Zeile so ändern, dass B sie besitzt | 401, gleiche Meldung |
403 mit vollständigem with check; 200 mit with check (true) |
| Zeile anlegen, die alle lesen dürfen | 201, und ein Gast konnte sie danach listen |
nur wenn eine Policy es erlaubt |
Appwrite beschränkt, was ein Nutzer vergeben kann, auf Rollen, die er selbst hat: any, users, den eigenen Nutzer und die eigenen Teams. Das blockierte den Zug „meine Zeile jemand anderem geben“, ohne dass ich eine Regel geschrieben hätte. Es hinderte A aber nicht daran, die eigene Zeile mit read("any") für alle zu veröffentlichen. Wenn eine Zeile nie öffentlich sein darf, sollte nicht der Client ihre Berechtigungen setzen; legen Sie sie stattdessen aus einer Server-Funktion an.
Bei Supabase ergibt sich, was die Policy sagt. Ein vollständiges with check lehnte den Zug mit 403 ab; with check (true), wie es manche Generatoren schreiben, ließ A die Zeile in Bs Konto verschieben.
Kann ein Nutzer der Firma eines anderen beitreten?
Bei Supabase besteht der Teamzugriff meist aus einer memberships-Tabelle plus Policies. In meinem Lauf ließ die Insert-Policy, die viele Tutorials vorschlagen, with check (user_id = auth.uid()), A sich selbst zu Firma 2 hinzufügen (201 Created) und danach deren Gehaltsprojekt lesen.
Appwrite hat Teams eingebaut. B legte ein Team an; A versuchte, sich als Mitglied und als Owner hinzuzufügen. Beide Anfragen lieferten 404 team_not_found: A konnte das Team nicht einmal sehen. Die Teammitgliedschaft entscheidet der Server, nicht eine Regel, die Sie schreiben, daher ist dieses Leck hier nicht möglich. Damit Zeilen geteilt werden, brauchen sie weiterhin read("team:…")-Berechtigungen, und die setzen Sie selbst.
Was öffnet ein Server-Key?
| Key | Appwrite | Supabase (aus meinem Test Anon- vs. Service-Role-Key) |
|---|---|---|
| Voller Server-Key | 200, alle 4 Zeilen beider Nutzer |
alle 3 Zeilen, RLS übersprungen |
Key mit engem Scope (users.read) |
401, missing scope rows.read |
keine Scope-Keys |
Ein Supabase Secret Key entspricht service_role, und die hat BYPASSRLS; was sie öffnet, steht in Anon Key vs. Service-Role-Key. Auch ein Appwrite-Key ignoriert Zeilenberechtigungen, aber nur für die Scopes, mit denen er angelegt wurde; ein Key für einen Job, der Nutzer verwaltet, kann Ihre Tabellen also nicht lesen. Jeder der beiden Keys in einem Browser-Bundle ist ein vollständiges Leck dessen, was er erreicht.
Zwei kleinere Befunde zu Appwrite: Ein Gast und ein angemeldeter Nutzer, die die Liste der Tabellen abfragten, bekamen beide 401 (der Aufruf braucht tables.read, das nur Keys haben), und read("users") bedeutet jedes angemeldete Konto, dieselbe Falle wie auth.uid() is not null bei Supabase, wenn die Registrierung offen ist.
Welches ist sicherer?
Keines ist standardmäßig sicher oder unsicher; beide starteten geschlossen. Sie unterscheiden sich darin, wo sich ein Fehler verstecken kann:
| Frage | Appwrite | Supabase |
|---|---|---|
| Wo die Zugriffsregeln liegen | Rollenlisten an Tabellen und Zeilen | SQL-Grants und Policies |
| Ein abgelehnter nutzerübergreifender Schreibzugriff | Fehler (401 / 404) |
200 mit null Zeilen bei Update und Delete |
| Zeilen an andere vergeben | Nur an Rollen, die der Nutzer hat | Was immer die Policy erlaubt |
| Regeln auf Spaltenwerte, MFA-Stufe | Nicht in Berechtigungen; braucht Servercode | In der Policy, z. B. aal2 für sensible Zeilen |
| Server-Keys | Mit Scopes | Secret Key überspringt alle Policies |
Appwrites Modell ist enger, und in meinem Lauf wehrte das enge Modell jeden nutzerübergreifenden Trick ab, ohne dass ich etwas schreiben musste. Supabases Modell ist SQL, es kann also Regeln ausdrücken, die Appwrite-Berechtigungen nicht können, etwa „nur mit MFA“ oder „nur Zeilen der Firmen, in denen ich Mitglied bin“, und es kann ebenso die falsche Regel ausdrücken. Wenn Ihr Team die Policies nicht lesen wird, verzeihen Appwrites Standardwerte mehr. Wenn Sie Regeln brauchen, die von den Daten abhängen, lässt Supabase Sie sie schreiben, und dann müssen sie getestet werden; meine Methode steht in Supabase RLS testen.
Wie prüfen Sie ein Supabase-Projekt?
Der kostenlose Supabase-Sicherheitscheck liest Ihr Projekt nur lesend und meldet Tabellen mit ausgeschaltetem RLS, Policies, die immer wahr sind, Schreib-Policies für Gäste und Grants, die mehr geben, als die Policies vermuten lassen. Was Supabase für Sie abdeckt und was es Ihnen überlässt, steht in Wie sicher ist Supabase?. Ein Appwrite-Pendant gibt es darin nicht; bei Appwrite führen Sie die obigen Anfragen als zwei Nutzer aus. Für Firebase habe ich dieselbe Art von Angriffen in Firebase Security Rules Beispiele durchgespielt.
Grenzen. Ein selbst gehostetes Appwrite 2.3.0 auf einem Laptop und ein lokaler Supabase-Stack, sechs Tage auseinander, ausgedachte Zeilen. Appwrite Cloud kann sich vom selbst gehosteten unterscheiden. Storage, Functions, Realtime, Auth-Einstellungen und Compliance-Zertifikate sind hier nicht verglichen.
Ein Backend für eine App mit privaten Daten gesucht?
Gern schaue ich mit Ihnen auf Ihr Zugriffsmodell. Ohne Verkaufsgespräch.
Kontakt aufnehmen