TL;DR
Les deux verrouillent une nouvelle table par défaut : Appwrite a répondu 401 à un invité et à un utilisateur connecté, et Supabase répond 401/403 tant que vous n’accordez pas la table aux rôles de l’API. La différence apparaît quand une règle est fausse ou qu’une requête franchit la frontière entre utilisateurs. Sur Supabase, la mise à jour de la ligne d’un autre a renvoyé 200 [] et une politique d’insertion trop lâche a laissé un utilisateur rejoindre une autre entreprise ; sur Appwrite, chaque tentative entre utilisateurs a renvoyé 401 ou 404 dans mon essai, et un utilisateur ne pouvait accorder sa ligne qu’à lui-même ou à tout le monde. Les clés serveur d’Appwrite ont des scopes ; une clé secrète Supabase ignore toutes les politiques.
Je suis Denys, ingénieur full-stack et IA. La plupart des applications que je construis ou répare tournent sur Supabase, et les clients qui hésitent encore sur le backend me demandent comment Appwrite se compare côté sécurité. Une liste de fonctionnalités n’y répond pas ; j’ai donc envoyé les mêmes requêtes aux deux.
Comment j’ai testé. Appwrite 2.3.0, auto-hébergé avec Docker le 30 octobre 2026, via son API REST en invité, avec deux utilisateurs connectés A et B, et avec deux clés serveur. Côté Supabase, c’est mon essai sur une pile locale (CLI 2.109.1, Postgres 17.6) du 24 octobre 2026 avec les mêmes rôles, décrit en détail dans Supabase RLS policy : exemples testés via l’API. Des lignes inventées des deux côtés.
Comment Appwrite et Supabase protègent-ils les données ?
Supabase protège les tables avec Postgres : un grant décide si les rôles de l’API atteignent la table, et des politiques de row level security, écrites en SQL, décident quelles lignes chaque requête voit et écrit. Appwrite range les permissions à côté des données : une table a une liste de rôles autorisés à lire, créer, modifier ou supprimer, et, avec la sécurité par ligne activée, chaque ligne porte sa propre liste. Dans la documentation des permissions d’Appwrite, l’accès est seulement accordé, jamais refusé, et un utilisateur le doit au niveau de la table ou de la ligne, pas des deux.
La question n’est donc pas de savoir lequel a un contrôle d’accès. Les deux en ont. Elle est de savoir ce que chacun vous laisse rater.
Que fait chacun d’une nouvelle table ?
| Requête | Appwrite, aucune permission définie | Supabase, RLS activée, sans grant (24 oct.) |
|---|---|---|
| L’invité liste les lignes | 401 user_unauthorized |
401 permission denied |
| L’utilisateur connecté liste | 401 user_unauthorized |
403 permission denied |
| L’utilisateur connecté ajoute une ligne | 401 No permissions provided for action ‘create’ |
non testé |
Une table neuve est fermée des deux côtés. Sur Supabase, c’est nouveau : les tables créées après le 30 octobre 2026 ne reçoivent plus automatiquement de grants pour la Data API, ce que j’ai traité dans le changement de grants du 30 octobre.
Que se passe-t-il quand une table est ouverte à tous ?
Les deux réglages « ouverts » ne sont pas la même chose. Dans Appwrite, j’ai donné à la table une seule permission, read("any"). Sur Supabase, l’état ouvert courant est la RLS désactivée avec les grants par défaut toujours en place.
| Requête d’un invité | Appwrite, read("any") |
Supabase, RLS désactivée, grants par défaut (24 oct.) |
|---|---|---|
| Lister les lignes | 200, les deux lignes |
200, les deux lignes |
| Supprimer une ligne | 401 |
200, les deux lignes supprimées |
Le « public » d’Appwrite signifie lecture seule tant que vous n’accordez pas aussi l’écriture. Une table Supabase sans RLS est lisible et modifiable par quiconque détient la clé publishable, c’est pourquoi « RLS disabled in public » est la première chose que je vérifie.
Comment fonctionnent les « lignes personnelles » de chacun ?
Sur Appwrite, j’ai activé la sécurité par ligne et donné à la table une seule permission, create("users"). Quand A a créé une ligne sans nommer de permissions, Appwrite les a définies lui-même : read, update et delete pour l’utilisateur A seul. Sur Supabase, la même intention demande quatre politiques sur (select auth.uid()) = user_id.
| Requête | Appwrite (30 oct.) | Supabase (24 oct.) |
|---|---|---|
| L’invité liste les lignes | 200, 0 ligne |
401 (pas de grant pour anon) |
| A liste les lignes | 200, 2 lignes de A |
200, 2 lignes de A |
| A lit la ligne de B | 404 row_not_found |
absente de la liste |
| A modifie la ligne de B | 401 |
200 [] |
| A supprime la ligne de B | 401 |
200 [] |
Aucun des deux n’a laissé fuiter la ligne de B. La différence tient à la réponse. Supabase filtre les lignes qu’une politique cache : une mise à jour qui ne correspond à rien est donc un succès avec zéro ligne ; une application qui lit « pas d’erreur » comme « enregistré » dit à l’utilisateur que sa modification a fonctionné. Appwrite refuse avec une erreur que l’application ne peut pas manquer.
Un utilisateur peut-il céder ses données à quelqu’un d’autre ?
C’est là que le code généré se trompe le plus souvent ; je l’ai donc essayé sur les deux.
| Requête de A | Appwrite | Supabase (24 oct.) |
|---|---|---|
| Créer une ligne lisible par B | 401, Permissions must be one of: any, users, user:A… |
dépend de la politique |
| Modifier sa ligne pour que B la possède | 401, même message |
403 avec un with check complet ; 200 avec with check (true) |
| Créer une ligne lisible par tous | 201, puis un invité l’a listée |
seulement si une politique l’autorise |
Appwrite limite ce qu’un utilisateur peut accorder aux rôles qu’il détient lui-même : any, users, son propre utilisateur et ses propres équipes. Cela a bloqué le geste « donner ma ligne à quelqu’un d’autre » sans qu’aucune règle ne vienne de moi. Cela n’a pas empêché A de publier sa propre ligne pour tout le monde avec read("any"). Si une ligne ne doit jamais être publique, ce n’est pas au client de définir ses permissions ; créez-la plutôt depuis une fonction serveur.
Sur Supabase, le résultat est ce que dit la politique. Un with check complet a refusé le geste avec 403 ; with check (true), que certains générateurs écrivent, a laissé A déplacer la ligne dans le compte de B.
Un utilisateur peut-il rejoindre l’entreprise d’un autre ?
Sur Supabase, l’accès d’équipe est en général une table memberships plus des politiques. Dans mon essai, la politique d’insertion que suggèrent beaucoup de tutoriels, with check (user_id = auth.uid()), a laissé A s’ajouter à l’entreprise 2 avec 201 Created, puis lire son projet de paie.
Appwrite a des équipes intégrées. B a créé une équipe ; A a essayé de s’y ajouter comme membre puis comme propriétaire. Les deux requêtes ont renvoyé 404 team_not_found : A ne pouvait même pas voir l’équipe. L’appartenance à une équipe est décidée par le serveur, pas par une règle que vous écrivez ; cette fuite-là n’est donc pas possible. Les lignes ont toujours besoin de permissions read("team:…") pour être partagées, et celles-là, c’est vous qui les définissez.
Qu’ouvre une clé serveur ?
| Clé | Appwrite | Supabase (d’après mon test clé anon vs service role) |
|---|---|---|
| Clé serveur complète | 200, les 4 lignes des deux utilisateurs |
les 3 lignes, RLS ignorée |
Clé à scope étroit (users.read) |
401, missing scope rows.read |
pas de clés à scope |
Une clé secrète Supabase correspond à service_role, qui a BYPASSRLS ; ce qu’elle ouvre est dans clé anon vs clé service role. Une clé Appwrite ignore aussi les permissions de lignes, mais seulement pour les scopes avec lesquels elle a été créée : une clé pour une tâche qui gère des utilisateurs ne peut donc pas lire vos tables. L’une ou l’autre clé dans un bundle de navigateur est une fuite totale de tout ce qu’elle atteint.
Deux constats plus modestes sur Appwrite : un invité et un utilisateur connecté qui demandaient la liste des tables ont tous deux reçu 401 (l’appel exige tables.read, que seules les clés ont), et read("users") veut dire n’importe quel compte connecté, le même piège que auth.uid() is not null sur Supabase quand l’inscription est ouverte.
Lequel est le plus sûr ?
Aucun n’est sûr ou non par défaut ; les deux sont partis fermés. Ils diffèrent par l’endroit où une erreur peut se cacher :
| Question | Appwrite | Supabase |
|---|---|---|
| Où vivent les règles d’accès | Listes de rôles sur tables et lignes | Grants SQL et politiques |
| Une écriture refusée entre utilisateurs | Erreur (401 / 404) |
200 avec zéro ligne pour update et delete |
| Accorder des lignes à d’autres | Seulement aux rôles que l’utilisateur détient | Ce que la politique autorise |
| Règles sur valeurs de colonnes, niveau MFA | Pas dans les permissions ; code serveur requis | Dans la politique, p. ex. aal2 pour les lignes sensibles |
| Clés serveur | Avec scopes | La clé secrète ignore toutes les politiques |
Le modèle d’Appwrite est plus étroit, et dans mon essai ce modèle étroit a refusé chaque astuce entre utilisateurs sans que j’écrive quoi que ce soit. Celui de Supabase est du SQL : il peut exprimer des règles que les permissions d’Appwrite ne peuvent pas, comme « seulement avec MFA » ou « seulement les lignes des entreprises dont je suis membre », et il peut aussi exprimer la mauvaise règle. Si votre équipe ne lira pas les politiques, les valeurs par défaut d’Appwrite pardonnent davantage. Si vous avez besoin de règles qui dépendent des données, Supabase vous laisse les écrire, et il faut alors les tester ; ma méthode est dans comment tester Supabase RLS.
Comment vérifier un projet Supabase ?
L’audit de sécurité Supabase gratuit lit votre projet en lecture seule et signale les tables sans RLS, les politiques toujours vraies, les politiques d’écriture pour les invités et les grants qui donnent plus que les politiques ne le laissent croire. Ce que Supabase couvre pour vous et ce qu’il vous laisse est dans Supabase est-il sûr ?. Il n’y a pas d’équivalent pour Appwrite ; pour Appwrite, exécutez les requêtes ci-dessus avec deux utilisateurs. Pour Firebase, j’ai mené le même genre d’attaques dans exemples de règles de sécurité Firebase.
Limites. Un Appwrite 2.3.0 auto-hébergé sur un ordinateur portable et une pile Supabase locale, à six jours d’écart, des lignes inventées. Appwrite Cloud peut différer de l’auto-hébergé. Storage, fonctions, realtime, réglages d’authentification et certificats de conformité ne sont pas comparés ici.
Vous choisissez un backend pour une application avec des données privées ?
Je regarde volontiers votre modèle d’accès avec vous. Sans démarchage.
Me contacter