Fundamentos de seguridad
Supabase "permission denied for schema public": tres causas
Supabase "permission denied for schema public" es un grant que falta en el esquema. Qué lo quitó y la línea que lo devuelve.

En resumen
- Supabase "permission denied for schema public" significa que el rol que hace la petición no tiene derecho a entrar en el esquema public, así que Postgres rechazó la petición antes de mirar una sola tabla.
- Todo proyecto de Supabase empieza con ese derecho concedido a anon, authenticated y service_role. Si tu app recibe este error, algo lo quitó: el esquema se borró y se volvió a crear, o un script revocó el grant.
- Devolverlo es una línea de SQL. El grant solo deja a un rol llegar a tus tablas, y Row Level Security sigue decidiendo qué filas recibe cada visitante, así que revisa esas reglas antes de darlo por arreglado.
Ayer tu app funcionaba. Hoy cada pantalla que lee de Supabase sale vacía o
muestra un error, y la consola del navegador tiene la misma línea para todas:
permission denied for schema public. Abres el panel de Supabase y todas las
tablas siguen ahí. El SQL Editor las lee sin ninguna queja.
La respuesta que hay detrás de esa línea suele llegar como un 401 o un 403. Recortada a los dos campos que importan, dice esto:
{
"code": "42501",
"message": "permission denied for schema public"
}
Supabase "permission denied for schema public" viene de Postgres, la base de
datos sobre la que funciona Supabase, y su lista de causas es corta. Todo
proyecto de Supabase se crea con un grant que deja entrar a tu app en el esquema
public. Alguien, o algo, lo ha quitado, y devolverlo es una línea de SQL.
Esta es la parte que es fácil entender al revés: ese grant es la puerta principal del edificio, y las cerraduras de tus datos están más adentro. Si alguien lo quitó para dejar fuera a los extraños, dejó fuera a tu app con ellos. Y que reabrirla sea seguro depende de esas cerraduras interiores, así que son lo que hay que revisar antes de darlo por arreglado.
¿Qué significa "permission denied for schema public"?
Significa que el rol que hace la petición no tiene derecho a entrar en el
esquema public, así que Postgres la rechazó antes de mirar una sola tabla.
Un esquema es una carpeta de tablas con nombre dentro de tu base de datos.
public es donde Lovable, Bolt, v0, Cursor, Claude Code y Replit ponen tus
tablas si nadie les dice otra cosa. Un rol es el tipo de visitante del que
viene una petición: anon es alguien que no ha iniciado sesión,
authenticated es alguien que sí, y service_role es tu propio código de
servidor usando la clave secreta. Un grant da a un rol un permiso sobre una
cosa.
El permiso del que habla este error se llama USAGE, y la
documentación de Postgres
lo describe como el derecho a buscar ("look up") objetos dentro del esquema. Un
rol sin él no llega a ninguna tabla de ese esquema, digan lo que digan los
ajustes de la propia tabla.
Si la versión de tabla de este error
te enseñó a pensar en cada tabla como una habitación con una puerta para cada
rol, el esquema es el edificio donde están esas habitaciones, y USAGE es su
puerta principal. permission denied for schema public significa que tu
petición está de pie fuera de ella. Da igual qué habitaciones tengan la puerta
abierta, porque la petición nunca llega a ninguna habitación.
Supabase da esa puerta principal a los tres roles cuando se crea un proyecto. La imagen de base de datos de Supabase ejecuta esta línea en su primer script de configuración:
grant usage on schema public to postgres, anon, authenticated, service_role;
Así que si tu app recibe este error, el grant estuvo ahí y ya no está.
¿Por qué el SQL Editor sigue funcionando?
Porque el SQL Editor no usa ninguno de los roles de tu app. Una
guía de resolución de problemas de Supabase
dice que una consulta lanzada desde ahí es "routed through the postgres role
at the database level", es decir, que pasa por el rol postgres a nivel de base
de datos.
postgres es el rol dueño de tus tablas, y si el propio esquema se volvió a
crear desde el panel o lo hizo tu builder, también es dueño de él. Un dueño no
necesita ningún grant para entrar en su propio edificio. Así que el panel
muestra todas las tablas, un select en el SQL Editor devuelve todas las filas,
y la misma petición desde tu app se queda en la puerta principal. Nada en esas
pantallas te dice que los roles de tu app han perdido la forma de entrar.
Para ver lo que ve tu app, pregunta a Postgres directamente por sus roles. Pega esto en el SQL Editor:
select rolname,
has_schema_privilege(rolname, 'public', 'USAGE') as can_enter
from pg_roles
where rolname in ('anon', 'authenticated', 'service_role');
Cada fila debería decir true. Una fila que dice false es un rol que usa tu
app y que ya no puede entrar.
El grant abre el edificio, y las cerraduras están dentro
USAGE deja a un rol llegar a las tablas de un esquema. No dice nada sobre
cuáles de esas tablas puede usar el rol, ni qué filas recibe.
Detrás de la puerta principal hay dos comprobaciones más, y las dos pertenecen a
cada tabla. El grant propio de la tabla decide si un rol puede usar esa tabla, y
cuando falta es lo que informa permission denied for table. Después, Row Level
Security, las reglas por fila que quizá ya conozcas, decide qué filas recibe ese
rol. Postgres pregunta las tres en ese orden y se detiene en el primer rechazo.
Ese orden es lo que convierte el grant del esquema en una mala cerradura. Para un
rol es todo o nada: quítaselo a anon y cada pantalla que ve un visitante sin
sesión deja de funcionar a la vez, incluida una página de inicio de sesión que
lea cualquier cosa de tus tablas. Quítaselo a authenticated y pasa lo mismo a
todos los que han iniciado sesión. La documentación de Postgres añade que
revocarlo "is not a completely secure way to prevent object access", es decir,
que no es una forma completamente segura de impedir el acceso, y que un rol sin
él todavía puede leer los nombres de tus tablas en los catálogos del sistema.
Si estás viendo este error, tu app necesita el rol que fue rechazado.
Causa 1: el esquema se borró y se volvió a crear
Esta causa es la que más se lleva por delante. Alguien ejecutó estas dos líneas, o algo que hace lo mismo:
drop schema public cascade;
create schema public;
La primera línea elimina el esquema y, por el cascade, cada tabla, vista y
función que había dentro. La segunda crea un public nuevo y vacío. Un esquema
nuevo pertenece al rol que lo creó, y la documentación de Postgres dice con
claridad que nadie más recibe ningún privilegio sobre un esquema por defecto. El
grant que Supabase ejecutó al crear tu proyecto pertenecía al esquema viejo y se
fue con él, y Supabase no vuelve a ejecutar su script de configuración.
Esas dos líneas son la forma más corta de vaciar una base de datos Postgres, así que es un paso que un asistente de IA puede dar cuando le pides empezar la base de datos de cero o hacer que una migración que falla pase limpia.
Después tus tablas vuelven si las migraciones de la app se ejecutan otra vez. Una migración es uno de los archivos de cambios de base de datos que escribe tu builder, y al ejecutarlas todas se reconstruyen las tablas que describen. Lo que no vuelve es todo lo que nunca mencionan, y el grant de la puerta principal lo hizo Supabase antes de que existiera tu primera migración. Puede que las migraciones que escribió tu builder no lo contengan en absoluto.
Otras dos cosas se van con el esquema viejo:
- Los grants automáticos para tablas nuevas. Postgres los guarda como privilegios por defecto (default privileges) del esquema, y se borran junto con él. Las tablas creadas después empiezan sin grant para ninguno de los roles de tu app.
- Las filas. Las migraciones reconstruyen tablas vacías. Lo que guardaban las viejas solo vuelve si alguien restauró una copia hecha antes del reinicio.
Si tus tablas han vuelto vacías, encontrar esa copia va antes que cualquier otra cosa de esta página.
Causa 2: un revoke pensado para proteger
La segunda forma es deliberada. Alguien leyó que cada tabla de public es
accesible a través de la Data API de Supabase, la dirección web por la que tu
app lee y escribe sus datos, y revocó el grant del esquema para cerrarla.
La preocupación de fondo es razonable, y la herramienta elegida es la
equivocada. La guía de Supabase para
proteger la Data API
nunca toca el grant del esquema. Lo que revoca es el acceso automático que
reciben las tablas nuevas de public, de modo que cada tabla nueva sigue cerrada
hasta que alguien le da acceso a propósito. Supabase lo activa para todos los
proyectos existentes el 30 de octubre de 2026.
Si un script que ejecutaste, o un asistente al que pediste reforzar tu base de datos, quitó el grant del esquema, la consulta de comprobación de arriba muestra qué roles lo perdieron. Devuélveselo a los que usa tu app y luego protege las cosas como describen las secciones de abajo.
Causa 3: el error viene de una migración
Cuando el mensaje aparece mientras algo está creando tablas, como una migración,
un despliegue, o Prisma o Drizzle preparando tu base de datos, el permiso que
falta es otro. Crear cualquier cosa en un esquema necesita CREATE sobre él, y
el texto del error cuando falta CREATE es la misma línea.
Desde Postgres 15, los roles normales ya no reciben CREATE sobre public por
defecto. La
documentación de Postgres
dice que solo las bases de datos actualizadas desde la versión 14 o anteriores
se lo siguen dando a todo el mundo. Así que un usuario de base de datos aparte,
creado para una herramienta de migraciones, recibe el rechazo en su primerísima
tabla. La guía de Prisma de
Supabase crea un usuario prisma, y su configuración incluye
grant create on schema public to prisma.
La solución es un grant de create para ese único usuario de migraciones. Nadie
que visite tu app necesita crear tablas, así que anon y authenticated nunca
reciben uno.
Cómo devolver el grant sin riesgo
Ejecuta una línea, confirma que los tres roles pueden entrar, recarga tu app y lee lo que dice a continuación.
La línea, en el SQL Editor:
grant usage on schema public to anon, authenticated, service_role;
Quédate en usage. grant all on schema public también quita el error, y en
un esquema all significa usage más create, el derecho a añadir tablas
propias. Supabase aconseja dar a cada rol
lo mínimo que necesita,
y los roles de tus visitantes solo necesitan entrar.
La consulta de comprobación de antes debería decir ahora true para los tres
roles. Recarga tu app y pasará una de dos cosas:
- Funciona. La puerta principal era lo único que faltaba.
- Un mensaje nuevo nombra una tabla.
permission denied for tablesignifica que también falta el grant propio de esa tabla, que es lo esperable después de la primera causa, porque las tablas reconstruidas nunca recibieron los grants automáticos. Da a cada tabla su grant, Row Level Security y una regla en un solo cambio.
Resiste la tentación de dar a cada rol todo sobre cada tabla para que desaparezca
el segundo mensaje. Una tabla con un grant para anon y Row Level Security
desactivado entrega todas sus filas a cualquiera que las pida, y en tu app nada
se verá distinto.
Si el mensaje nombra otro esquema, como permission denied for schema api, ese
esquema lo creó alguien, y nunca recibió los grants con los que empieza
public. La guía de Supabase para
usar esquemas propios
enumera las líneas que necesita.
Qué proteger en su lugar
Los controles que mantienen a los extraños lejos de tus datos pertenecen a cada tabla, y la guía de seguridad de Supabase los nombra:
- Un grant por tabla, para los roles que lo necesitan. Una tabla de pedidos
no necesita ningún grant para
anon. - Row Level Security en cada tabla, con una regla.Cómo activarlo y comprobar que aguanta.
- Tablas nuevas cerradas por defecto. Revocar los grants automáticos lo consigue hoy, y el 30 de octubre lo hace por ti.
- Las tablas internas fuera de la Data API. La guía de Supabase describe poner lo que lee tu app en un esquema dedicado, de modo que todo lo demás quede en esquemas que la Data API no sirve.
Después mira el resultado desde fuera, porque ninguna de esas pantallas muestra lo que recibe un extraño. Nuestro escaneo gratuito pregunta a cada tabla que tu app nombra cuántas filas recibiría un visitante sin sesión, lee la cifra sin descargar ni una fila y tarda unos 20 segundos, sin cuenta: escanea tu app.
Conservar tus tablas, y lo que hay en ellas
Después de hoy aún pueden salir mal dos cosas: un cambio posterior abre una tabla a los extraños, y un reinicio como el de la primera causa se lleva tus filas.
Reeve Monitor sigue comprobando desde fuera:
- la comprobación de seguridad completa cada hora, en hasta tres apps
- un mensaje cuando un resultado cambia, para que una tabla que se abrió tras una migración no espere a que lo mires
- si tu app está en línea
- un informe mensual, en lenguaje claro, de lo que ha visto
Reeve Care guarda una copia de lo que hay en tus tablas:
- una copia cifrada de tu base de datos de Supabase según un calendario, en el almacenamiento propio de Reeve en la UE, fuera de tu cuenta de Supabase
- cada copia verificada antes de contar, contando las filas de cada tabla
- también tus archivos subidos, en cuanto conectas una credencial de Storage
- una restauración con un clic, con una copia de seguridad del estado actual hecha antes
- todo lo que hace Monitor
Qué hacer ahora
Qué hacer
- Ejecuta la consulta de comprobación en el SQL Editor. Cada rol que diga
falsees uno que tu app ha perdido. - Devuelve el grant con
grant usage on schema public to anon, authenticated, service_role;y quédate enusage. - Si el esquema se borró, mira si tus tablas están vacías y busca una copia hecha antes del reinicio.
- Si después aparece un error de tabla, da a esa tabla su grant, Row Level Security y una regla a la vez.
- Protege tabla por tabla, donde se puede ajustar, y deja en paz el grant del esquema.
- Si el error vino de una migración, da
createal usuario de migraciones y a nadie más.
Empieza por la consulta de comprobación, porque un solo resultado te dice qué roles de tu app perdieron la forma de entrar. Luego, con la puerta abierta otra vez, escanea tu app para ver lo que le entrega a un extraño.
Preguntas frecuentes
¿Qué significa "permission denied for schema public" en Supabase?
Significa que el rol que hace la petición, normalmente anon o authenticated, no tiene derecho a entrar en el esquema public, así que Postgres la rechazó antes de mirar ninguna tabla. Lleva el código 42501 y suele llegar a tu app como un 401 o un 403. Todo proyecto de Supabase empieza con ese derecho concedido, así que el error significa que alguien o algo lo quitó.
¿Es seguro conceder usage sobre el esquema public?
Sí. Todo proyecto de Supabase empieza exactamente con ese grant. Solo deja a un rol llegar a las tablas del esquema: cada tabla sigue necesitando su propio grant, y Row Level Security sigue decidiendo qué filas recibe cada visitante. Lo que merece una mirada son las tablas de detrás, porque una con un grant para anon y Row Level Security desactivado entrega todas sus filas a cualquiera que las pida.
¿En qué se diferencia de "permission denied for table"?
El error del esquema detiene la petición antes de que llegue a ninguna tabla, así que todas las tablas del esquema fallan a la vez. El error de tabla afecta a una sola tabla que no tiene grant para ese rol, y el resto de tu app sigue funcionando. Los dos llevan el código 42501, así que lo que los distingue son las palabras del mensaje.
¿Por qué mis tablas funcionan en el SQL Editor de Supabase y no en mi app?
Porque el SQL Editor ejecuta tus consultas con el rol postgres, que es el dueño de tus tablas y no necesita el grant que han perdido los roles de tu app. Tu app usa anon para los visitantes sin sesión y authenticated para los que han iniciado sesión. Pregunta a Postgres por esos dos roles directamente, con has_schema_privilege, para ver lo que ve tu app.
¿Qué debería proteger en lugar del grant del esquema?
Las tablas. Da a cada tabla un grant solo para los roles que lo necesitan, activa Row Level Security con una regla en cada tabla y deja que las tablas nuevas empiecen sin ningún acceso. Supabase convierte esa última parte en la opción por defecto para los proyectos existentes el 30 de octubre de 2026.