Em resumo
Ambos bloqueiam uma tabela nova por omissão: o Appwrite respondeu 401 a um convidado e a um utilizador autenticado, e o Supabase responde 401/403 até dar a tabela aos roles da API. A diferença aparece quando uma regra está errada ou um pedido passa de um utilizador para outro. No Supabase, um update na linha de outro utilizador devolveu 200 [] e uma policy de insert demasiado larga deixou um utilizador juntar-se a outra empresa; no Appwrite, todas as tentativas entre utilizadores na minha execução acabaram em 401 ou 404, e um utilizador não podia dar a sua linha a ninguém senão a si próprio ou a todos. As chaves de servidor do Appwrite têm scopes; uma secret key do Supabase ignora todas as policies.
Sou o Denys, engenheiro full-stack e de IA. A maioria das apps que construo ou corrijo corre em Supabase, e os clientes que ainda estão a escolher um backend perguntam-me como se compara o Appwrite em segurança. Uma lista de funcionalidades não responde a isso, por isso enviei aos dois os mesmos pedidos.
Como testei. Appwrite 2.3.0, self-hosted com Docker a 30 de outubro de 2026, pela sua API REST como convidado, como dois utilizadores autenticados A e B e com duas chaves de servidor. O lado Supabase é a minha execução numa stack local (CLI 2.109.1, Postgres 17.6) de 24 de outubro de 2026 com os mesmos roles, descrita por inteiro em Exemplos de RLS policy no Supabase. Linhas inventadas nos dois.
Como protegem os dados o Appwrite e o Supabase?
O Supabase protege as tabelas com o Postgres: um grant decide se os roles da API chegam sequer a uma tabela, e as policies de row level security, escritas em SQL, decidem que linhas cada pedido vê e escreve. O Appwrite guarda as permissões junto dos dados: uma tabela tem uma lista de roles que podem ler, criar, atualizar ou apagar, e com a row security ligada cada linha traz a sua própria lista. Na documentação de permissões do Appwrite o acesso só é concedido, nunca negado, e o utilizador precisa dele ao nível da tabela ou da linha, não dos dois.
Portanto, a pergunta não é qual dos dois tem controlo de acessos. Ambos têm. É o que cada um lhe deixa errar.
O que faz cada um com uma tabela nova?
| Pedido | Appwrite, sem permissões definidas | Supabase, RLS ligado, sem grant (24 out) |
|---|---|---|
| Convidado lista linhas | 401 user_unauthorized |
401 permission denied |
| Utilizador autenticado lista linhas | 401 user_unauthorized |
403 permission denied |
| Utilizador autenticado adiciona linha | 401 No permissions provided for action ‘create’ |
não testado |
Uma tabela recém-criada está fechada nos dois. No Supabase isso é novidade: as tabelas criadas depois de 30 de outubro de 2026 deixam de receber grants da Data API automaticamente, o que expliquei em Supabase, 30 de outubro: o que falha com os novos grants.
O que acontece quando uma tabela está aberta a todos?
As duas definições «abertas» não são a mesma coisa. No Appwrite dei à tabela uma só permissão, read("any"). No Supabase, o estado aberto mais comum é o RLS desligado com os grants por omissão ainda no sítio.
| Pedido de um convidado | Appwrite, read("any") |
Supabase, RLS desligado, grants por omissão (24 out) |
|---|---|---|
| Listar linhas | 200, as duas linhas |
200, as duas linhas |
| Apagar uma linha | 401 |
200, as duas linhas apagadas |
O «público» do Appwrite é só de leitura, a não ser que dê também escrita. Uma tabela Supabase com RLS desligado é legível e gravável por qualquer pessoa com a publishable key, e é por isso que «RLS disabled in public» é a primeira coisa que verifico.
Como funcionam as «linhas próprias» em cada um?
No Appwrite liguei a row security e dei à tabela uma só permissão, create("users"). Quando A criou uma linha sem indicar permissões, o Appwrite definiu-as por si: read, update e delete só para o utilizador A. No Supabase a mesma intenção são quatro policies sobre (select auth.uid()) = user_id.
| Pedido | Appwrite (30 out) | Supabase (24 out) |
|---|---|---|
| Convidado lista | 200, 0 linhas |
401 (sem grant para anon) |
| A lista linhas | 200, as 2 linhas de A |
200, as 2 linhas de A |
| A lê a linha de B | 404 row_not_found |
não devolvida na lista |
| A atualiza linha de B | 401 |
200 [] |
| A apaga linha de B | 401 |
200 [] |
Nenhum deixou vazar a linha de B. A diferença é a resposta. O Supabase filtra as linhas que uma policy esconde, por isso um update que não apanhou nada é um sucesso com zero linhas; uma app que trata «sem erro» como «guardado» diz ao utilizador que a edição funcionou. O Appwrite recusa com um erro que a app não pode deixar de ver.
Um utilizador pode passar os seus dados a outra pessoa?
É aqui que o código gerado mais falha, por isso testei nos dois.
| Pedido de A | Appwrite | Supabase (24 out) |
|---|---|---|
| Criar linha legível pelo utilizador B | 401, Permissions must be one of: any, users, user:A… |
depende da policy |
| Mudar a própria linha para B ser o dono | 401, a mesma mensagem |
403 com um with check completo; 200 com with check (true) |
| Criar linha legível por todos | 201, e um convidado listou-a a seguir |
só se uma policy o permitir |
O Appwrite limita o que um utilizador pode conceder aos roles que ele próprio tem: any, users, o seu utilizador e as suas equipas. Isso bloqueou o gesto «dar a minha linha a outra pessoa» sem qualquer regra minha. Não impediu A de publicar a sua própria linha para todos com read("any"). Se uma linha nunca deve ser pública, não deve ser o cliente a definir as suas permissões; crie-a a partir de uma função de servidor.
No Supabase o resultado é o que a policy disser. Um with check completo recusou a mudança com 403; with check (true), que alguns geradores escrevem, deixou A mover a linha para a conta de B.
Um utilizador pode juntar-se à empresa de outra pessoa?
No Supabase, o acesso por equipa costuma ser uma tabela memberships mais policies. Na minha execução, a policy de insert que muitos tutoriais sugerem, with check (user_id = auth.uid()), deixou A adicionar-se à empresa 2 com 201 Created e depois ler o seu projeto de salários.
O Appwrite tem equipas integradas. B criou uma equipa; A tentou adicionar-se como membro e como dono. Os dois pedidos devolveram 404 team_not_found: A nem sequer via a equipa. A pertença a uma equipa é decidida pelo servidor, não por uma regra que escreva, por isso esta fuga em concreto não existe. As linhas continuam a precisar de permissões read("team:…") para serem partilhadas, e essas define-as você.
O que abre uma chave de servidor?
| Chave | Appwrite | Supabase (do meu teste anon vs service role) |
|---|---|---|
| Chave de servidor completa | 200, as 4 linhas dos dois utilizadores |
as 3 linhas, RLS ignorado |
Chave com scope estreito (users.read) |
401, missing scope rows.read |
não há chaves com scope |
Uma secret key do Supabase corresponde a service_role, que tem BYPASSRLS; o que abre está em anon key vs service role key. Uma chave do Appwrite também ignora as permissões de linha, mas só para os scopes com que foi criada, por isso uma chave para uma tarefa que gere utilizadores não consegue ler as suas tabelas. Qualquer das chaves num bundle de browser é uma fuga total de tudo o que ela alcança.
Duas constatações menores sobre o Appwrite: um convidado e um utilizador autenticado a pedir a lista de tabelas receberam ambos 401 (a chamada precisa de tables.read, que só as chaves têm), e read("users") significa qualquer conta autenticada, a mesma armadilha de auth.uid() is not null no Supabase quando o registo está aberto.
Qual é mais seguro?
Nenhum é seguro ou inseguro por omissão; ambos partiram fechados. Diferem no sítio onde um erro se pode esconder:
| Pergunta | Appwrite | Supabase |
|---|---|---|
| Onde vivem as regras de acesso | Listas de roles em tabelas e linhas | Grants e policies em SQL |
| Uma escrita negada entre utilizadores | Erro (401 / 404) |
200 com zero linhas em update e delete |
| Dar linhas a outros | Só aos roles que o utilizador tem | O que a policy permitir |
| Regras sobre valores de colunas, nível MFA | Não nas permissões; exige código de servidor | Na policy, p. ex. aal2 para linhas sensíveis |
| Chaves de servidor | Com scope | A secret key ignora todas as policies |
O modelo do Appwrite é mais estreito, e na minha execução o modelo estreito recusou todos os truques entre utilizadores sem eu escrever nada. O modelo do Supabase é SQL, por isso consegue exprimir regras que as permissões do Appwrite não conseguem, como «só com MFA» ou «só linhas das empresas a que pertenço», e também consegue exprimir a regra errada. Se a sua equipa não vai ler as policies, os valores por omissão do Appwrite perdoam mais. Se precisa de regras que dependam dos dados, o Supabase deixa-o escrevê-las, e depois têm de ser testadas; o método que uso está em como testar o Supabase RLS.
Como verificar um projeto Supabase?
A Auditoria de segurança Supabase gratuita lê o seu projeto só em leitura e assinala tabelas com RLS desligado, policies sempre verdadeiras, policies de escrita para convidados e grants que dão mais do que as policies deixam supor. O que o Supabase cobre por si e o que lhe deixa a si está em o Supabase é seguro?. Não existe equivalente para o Appwrite; no Appwrite, corra os pedidos acima como dois utilizadores. Para o Firebase, corri o mesmo tipo de ataques em exemplos de regras de segurança do Firebase.
Limites. Um Appwrite 2.3.0 self-hosted num portátil e uma stack Supabase local, com seis dias de intervalo, com linhas inventadas. O Appwrite Cloud pode diferir do self-hosted. Storage, funções, realtime, definições de auth e certificados de conformidade não são comparados aqui.
A escolher um backend para uma app com dados privados?
Terei gosto em ver o seu modelo de acessos consigo. Sem pitch.
Fale comigo