Supabase regala TRUNCATE y TRIGGER a anon por defecto en el esquema public
El ACL por defecto del esquema public otorga arwdDxtm a anon y authenticated en cada tabla nueva. Dos de esos privilegios no pasan por RLS.

Si tienes un proyecto en Supabase y nunca has mirado los privilegios del rol anon, hay una consulta que se ejecuta en quince segundos y conviene lanzar antes de seguir con lo que estabas haciendo:
SELECT grantee, privilege_type, count(*) AS n
FROM information_schema.role_table_grants
WHERE table_schema = 'public'
AND grantee IN ('anon','authenticated')
AND privilege_type IN ('TRUNCATE','TRIGGER')
GROUP BY 1, 2 ORDER BY 1, 2;
En el proyecto del autor de este análisis el resultado fue anon con TRUNCATE sobre 96 tablas y authenticated sobre 109. Ninguno de esos GRANT lo había escrito él.
El origen no es un descuido suyo. El esquema public de Supabase lleva una entrada en pg_default_acl que añade la cadena arwdDxtm a anon y authenticated en cada tabla que se cree a partir de ese momento. La D es TRUNCATE; la t, TRIGGER. La plataforma tomó la decisión antes de que el usuario escribiera su primera línea de SQL. Y revocar en las tablas existentes no cierra nada, porque la siguiente tabla nace otra vez con esos permisos.
Por qué RLS no protege aquí
La seguridad a nivel de fila de PostgreSQL cubre cuatro verbos: SELECT, INSERT, UPDATE y DELETE. TRUNCATE no está entre ellos. Vacía una tabla entera en una sola sentencia y solo responde a privilegios de tabla, así que la muralla de políticas que este desarrollador tardó un año en levantar queda por encima del agujero. TRIGGER va por el mismo camino: quien lo tiene puede colgar un trigger y ejecutar código como el propietario de la tabla.
Medida con honestidad, la gravedad no es que a alguien le vayan a vaciar la base de datos mañana. PostgREST no expone ningún verbo TRUNCATE ni un endpoint para crear triggers, de modo que la clave anon pública no llega hasta ahí a través de la API: hace falta una conexión real a la base de datos. Lo que hay es un boquete en los cimientos de la defensa en profundidad. Crees que el peor caso está acotado por RLS, y hay dos verbos que RLS no gobierna.
El chequeo estaba verde, y con razón
El autor tenía un verificador previo a cada commit cuya única misión era comprobar que toda migración que crea una tabla escribe sus tres líneas GRANT. Pasaba siempre. Pasaba correctamente: lee los ficheros de migración como texto y confirma que los GRANT están ahí. Todo el agujero vive justo en lo que nunca se escribió.
Al arreglarlo repitió el error. El bucle de revocación recorría relkind IN ('r','p') —tablas normales y particionadas, la lista que produce la cabeza cuando uno oye "tabla"— y la consulta de verificación unía pg_tables, que también lista solo tablas normales. La corrección no veía las vistas y la verificación compartía el mismo punto ciego, así que era incapaz de falsarla. Siete vistas seguían con TRUNCATE al día siguiente.
De ahí salen sus tres reglas: derivar el alcance de una consulta al catálogo y no de la memoria; que la consulta de aceptación no contenga ni una palabra que no esté en el invariante; y emparejar cada comprobación de "falta algo" con su gemela de "sobra algo". El desarrollo completo del caso incluye la migración de reparación.
Merece la pena revisar los ACL por defecto de cualquier plataforma gestionada que cree objetos en tu nombre, no solo en Postgres. Y queda por ver si Supabase cambia el ACL que trae public de fábrica o lo deja como está y lo documenta mejor.

