---
title: "Appwrite vs Supabase: los mismos ataques en ambos"
description: "Los mismos ataques de acceso contra Appwrite 2.3.0 y un Supabase local. Ambos bloquean tablas nuevas; difieren en los errores y en lo que abre una clave."
url: "https://madsalt.tech/es/blog/appwrite-vs-supabase/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/es/about/"
language: "es"
published: 2026-10-30
---

# Appwrite vs Supabase: los mismos ataques en ambos

En resumen

Ambos bloquean una tabla nueva por defecto: Appwrite respondió `401` a un invitado y a un usuario autenticado, y Supabase responde `401`/`403` hasta que concede la tabla a los roles de la API. La diferencia aparece cuando una regla está mal o una petición cruza usuarios. En Supabase, un update sobre la fila de otro devolvió `200 []` y una política de insert demasiado laxa dejó que un usuario se uniera a otra empresa; en Appwrite, todo intento entre usuarios de mi prueba volvió como `401` o `404`, y un usuario solo podía conceder su fila a sí mismo o a todo el mundo. Las claves de servidor de Appwrite tienen scopes; una clave secreta de Supabase se salta todas las políticas.

Soy Denys, ingeniero full-stack y de IA. La mayoría de las apps que construyo o reparo funcionan sobre Supabase, y los clientes que aún eligen backend me preguntan cómo se compara Appwrite en seguridad. Una lista de funciones no lo responde, así que envié a ambos las mismas peticiones.

**Cómo probé.** Appwrite 2.3.0, autoalojado con Docker el 30 de octubre de 2026, mediante su API REST como invitado, como dos usuarios autenticados A y B, y con dos claves de servidor. El lado de Supabase es mi prueba en un stack local (CLI 2.109.1, Postgres 17.6) del 24 de octubre de 2026 con los mismos roles, descrita al completo en [Políticas RLS de Supabase: ejemplos probados con la API](https://madsalt.tech/es/blog/supabase-rls-policy-examples/). Filas inventadas en ambos.

## ¿Cómo protegen Appwrite y Supabase los datos?

Supabase protege las tablas con Postgres: un grant decide si los roles de la API llegan siquiera a la tabla, y las políticas de row level security, escritas en SQL, deciden qué filas ve y escribe cada petición. Appwrite guarda los permisos junto a los datos: una tabla tiene una lista de roles que pueden leer, crear, actualizar o borrar, y con la seguridad por fila activada cada fila lleva su propia lista. En la [documentación de permisos de Appwrite](https://appwrite.io/docs/products/databases/tablesdb/permissions) el acceso solo se concede, nunca se deniega, y el usuario lo necesita a nivel de tabla o de fila, no en ambos.

Así que la pregunta no es cuál tiene control de acceso. Ambos lo tienen. Es qué le deja equivocarse a cada uno.

## ¿Qué hace cada uno con una tabla nueva?

| Petición                           | Appwrite, sin permisos definidos                  | Supabase, RLS activado, sin grant (24 oct.) |
| ---------------------------------- | ------------------------------------------------- | ------------------------------------------- |
| El invitado lista filas            | `401` user_unauthorized                           | `401` permission denied                     |
| Usuario autenticado lista filas    | `401` user_unauthorized                           | `403` permission denied                     |
| Usuario autenticado añade una fila | `401` No permissions provided for action 'create' | no probado                                  |

Una tabla recién creada está cerrada en ambos. En Supabase eso es nuevo: las tablas creadas después del 30 de octubre de 2026 ya no reciben grants de la Data API automáticamente, lo que traté en [el cambio de grants del 30 de octubre](https://madsalt.tech/es/blog/supabase-october-30-grant-change/).

## ¿Qué pasa cuando una tabla está abierta a todos?

Los dos ajustes «abiertos» no son lo mismo. En Appwrite di a la tabla un solo permiso, `read("any")`. En Supabase el estado abierto habitual es RLS desactivado con los grants por defecto aún puestos.

| Petición de un invitado | Appwrite, `read("any")` | Supabase, RLS desactivado, grants por defecto (24 oct.) |
| ----------------------- | ----------------------- | ------------------------------------------------------- |
| Listar filas            | `200`, las dos filas    | `200`, las dos filas                                    |
| Borrar una fila         | `401`                   | `200`, las dos filas borradas                           |

El «público» de Appwrite significa solo lectura salvo que también concedas escritura. Una tabla de Supabase con RLS desactivado la puede leer y escribir cualquiera que tenga la clave publicable, y por eso «RLS disabled in public» es lo primero que reviso.

## ¿Cómo funcionan las «filas propias» en cada uno?

En Appwrite activé la seguridad por fila y di a la tabla un permiso, `create("users")`. Cuando A creó una fila sin indicar permisos, Appwrite los puso solo: `read`, `update` y `delete` solo para el usuario A. En Supabase, la misma intención son cuatro políticas sobre `(select auth.uid()) = user_id`.

| Petición                 | Appwrite (30 oct.)  | Supabase (24 oct.)            |
| ------------------------ | ------------------- | ----------------------------- |
| El invitado lista filas  | `200`, 0 filas      | `401` (sin grant para `anon`) |
| A lista filas            | `200`, 2 filas de A | `200`, 2 filas de A           |
| A lee la fila de B       | `404` row_not_found | no aparece en la lista        |
| A actualiza la fila de B | `401`               | `200 []`                      |
| A borra la fila de B     | `401`               | `200 []`                      |

![La misma petición, dos backends. El usuario A edita una fila del usuario B, por la API REST de cada uno. Supabase, política solo dueño: Usuario A → editar la fila de B → 200 \[\] → 0 filas, sin error; Appwrite, seguridad por fila: Usuario A → editar la fila de B → 401 → error devuelto.](https://madsalt.tech/charts/es/appwrite-vs-supabase.svg)

*Figura 1. Mis pruebas: stack local de Supabase (CLI 2.109.1), 24 de octubre de 2026; Appwrite 2.3.0 autoalojado, 30 de octubre de 2026.*

Ninguno filtró la fila de B. La diferencia está en la respuesta. Supabase filtra las filas que una política oculta, de modo que un update que no coincide con nada es un éxito con cero filas; una app que toma «sin error» por «guardado» le dice al usuario que su cambio funcionó. Appwrite rechaza con un error que la app no puede pasar por alto.

## ¿Puede un usuario entregar sus datos a otra persona?

Aquí es donde el código generado falla con más frecuencia, así que lo probé en ambos.

| Petición de A                           | Appwrite                                               | Supabase (24 oct.)                                                |
| --------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------- |
| Crear una fila legible por B            | `401`, Permissions must be one of: any, users, user:A… | depende de la política                                            |
| Cambiar su fila para que B sea el dueño | `401`, mismo mensaje                                   | `403` con un `with check` completo; `200` con `with check (true)` |
| Crear una fila legible por todos        | `201`, y un invitado la listó después                  | solo si una política lo permite                                   |

Appwrite limita lo que un usuario puede conceder a los roles que él mismo tiene: `any`, `users`, su propio usuario y sus propios equipos. Eso bloqueó el movimiento «dar mi fila a otra persona» sin ninguna regla mía. No impidió que A publicara su propia fila para todos con `read("any")`. Si una fila nunca debe ser pública, el cliente no debería ser quien fije sus permisos; créela desde una función de servidor.

En Supabase el resultado es lo que diga la política. Un `with check` completo rechazó el movimiento con `403`; `with check (true)`, que algunos generadores escriben, dejó que A pasara la fila a la cuenta de B.

## ¿Puede un usuario unirse a la empresa de otro?

En Supabase, el acceso por equipos suele ser una tabla `memberships` más políticas. En mi prueba, la política de insert que sugieren muchos tutoriales, `with check (user_id = auth.uid())`, dejó que A se añadiera a la empresa 2 con `201 Created` y luego leyera su proyecto de nóminas.

Appwrite tiene equipos integrados. B creó un equipo; A intentó añadirse como miembro y como propietario. Ambas peticiones devolvieron `404 team_not_found`: A ni siquiera podía ver el equipo. La pertenencia a un equipo la decide el servidor, no una regla que usted escriba, así que esta fuga concreta no existe. Las filas siguen necesitando permisos `read("team:…")` para compartirse, y esos los fija usted.

## ¿Qué abre una clave de servidor?

| Clave                                   | Appwrite                             | Supabase (de mi prueba anon vs service role) |
| --------------------------------------- | ------------------------------------ | -------------------------------------------- |
| Clave de servidor completa              | `200`, las 4 filas de ambos usuarios | las 3 filas, RLS omitido                     |
| Clave con scope estrecho (`users.read`) | `401`, missing scope rows.read       | no hay claves con scope                      |

Una clave secreta de Supabase corresponde a `service_role`, que tiene `BYPASSRLS`; lo que abre está en [clave anon vs clave service role](https://madsalt.tech/es/blog/supabase-anon-key-vs-service-role-key/). Una clave de Appwrite también ignora los permisos de fila, pero solo para los scopes con los que se creó, así que una clave para una tarea que gestiona usuarios no puede leer sus tablas. Cualquiera de las dos claves en un bundle de navegador es una fuga total de lo que alcance.

Dos hallazgos menores sobre Appwrite: un invitado y un usuario autenticado que pidieron la lista de tablas recibieron ambos `401` (la llamada necesita `tables.read`, que solo tienen las claves), y `read("users")` significa cualquier cuenta autenticada, la misma trampa que `auth.uid() is not null` en Supabase cuando el registro está abierto.

## ¿Cuál es más seguro?

Ninguno es seguro o inseguro por defecto; ambos empezaron cerrados. Se distinguen en dónde puede esconderse un error:

| Pregunta                                   | Appwrite                                    | Supabase                                           |
| ------------------------------------------ | ------------------------------------------- | -------------------------------------------------- |
| Dónde viven las reglas de acceso           | Listas de roles en tablas y filas           | Grants SQL y políticas                             |
| Una escritura denegada entre usuarios      | Error (`401` / `404`)                       | `200` con cero filas en update y delete            |
| Conceder filas a otros                     | Solo a roles que el usuario tiene           | Lo que permita la política                         |
| Reglas sobre valores de columna, nivel MFA | No en permisos; requiere código de servidor | En la política, p. ej. `aal2` para filas sensibles |
| Claves de servidor                         | Con scopes                                  | La clave secreta se salta todas las políticas      |

El modelo de Appwrite es más estrecho, y en mi prueba ese modelo estrecho rechazó todos los trucos entre usuarios sin que yo escribiera nada. El de Supabase es SQL, así que puede expresar reglas que los permisos de Appwrite no pueden, como «solo con MFA» o «solo filas de las empresas a las que pertenezco», y también puede expresar la regla equivocada. Si su equipo no va a leer las políticas, los valores por defecto de Appwrite perdonan más. Si necesita reglas que dependan de los datos, Supabase le deja escribirlas, y entonces hay que probarlas; el método que uso está en [cómo probar Supabase RLS](https://madsalt.tech/es/blog/how-to-test-supabase-rls/).

## ¿Cómo comprobar un proyecto de Supabase?

El [verificador de seguridad de Supabase](https://madsalt.tech/es/tools/supabase-security-check/), gratuito, lee su proyecto en modo de solo lectura y señala las tablas con RLS desactivado, las políticas siempre verdaderas, las políticas de escritura para invitados y los grants que dan más de lo que las políticas sugieren. Qué cubre Supabase por usted y qué le deja a usted está en [¿es seguro Supabase?](https://madsalt.tech/es/blog/is-supabase-secure/). No hay equivalente para Appwrite; con Appwrite, ejecute las peticiones de arriba como dos usuarios. Para Firebase, ejecuté el mismo tipo de ataques en [ejemplos de reglas de seguridad de Firebase](https://madsalt.tech/es/blog/firebase-security-rules-examples/).

**Límites.** Un Appwrite 2.3.0 autoalojado en un portátil y un stack local de Supabase, con seis días de diferencia, filas inventadas. Appwrite Cloud puede diferir del autoalojado. Storage, functions, realtime, ajustes de auth y certificados de cumplimiento no se comparan aquí.

¿Elige backend para una app con datos privados?

Me gustaría revisar su modelo de acceso con usted. Sin pitch.

[Escríbame](https://madsalt.tech/es/#contact)
