En resumen
El 30 de octubre de 2026 Supabase deja de dar grants automáticos para anon, authenticated y service_role a las tablas nuevas de public en todos los proyectos existentes. Las tablas que ya tiene siguen funcionando. Cada tabla creada después responde 42501 permission denied a supabase-js y a su secret key hasta que una migración le dé el grant. Reproduje el cambio en local: las tablas nuevas, las vistas nuevas y las tablas antiguas que una migración borra y vuelve a crear fallan; las tablas existentes, las funciones RPC y los inserts con serial siguen funcionando. Compruebe con dos consultas SQL y un grep (más abajo) y corríjalo con grant, RLS y policy en la misma migración, nunca con grant all on all tables.
Mantengo un Supabase security checker gratuito que lee los grants de cada tabla expuesta, así que quise ver exactamente qué hace este cambio antes de que la gente empiece a tropezar con él. Levanté un stack local de Supabase, le puse los defaults antiguos, apliqué el cambio como lo describe Supabase y lancé una petición contra cada tipo de objeto que se me ocurrió. Este artículo cuenta lo que respondió y cómo comprobar su propio proyecto antes del día 30.
¿Qué cambia el 30 de octubre de 2026?
Hasta ahora, cada tabla creada en el esquema public recibía select, insert, update y delete para anon, authenticated y service_role mediante los default privileges de Postgres. Por eso una tabla nueva era accesible desde el navegador en cuanto existía, con RLS o sin él. El aviso de breaking change de Supabase quita esos defaults en tres pasos:
| Fecha | Qué ocurre |
|---|---|
| 28 de abril de 2026 | Los proyectos nuevos pueden desactivar “Automatically expose new tables” al crearse |
| 30 de mayo de 2026 | El nuevo comportamiento pasa a ser el predeterminado en los proyectos nuevos, de forma gradual durante unas semanas |
| 30 de octubre de 2026 | El nuevo comportamiento se aplica a todos los proyectos existentes |
En su proyecto el cambio son dos sentencias, las mismas que Supabase da para adoptarlo antes de tiempo:
alter default privileges for role postgres in schema public
revoke select, insert, update, delete on tables from anon, authenticated, service_role;
alter default privileges for role postgres in schema public
revoke usage, select on sequences from anon, authenticated, service_role;
Los default privileges solo afectan a los objetos que se crean después. Los grants que ya existen no se tocan, y por eso sus tablas actuales siguen funcionando. Lo que no cambia, según el aviso: los demás esquemas (auth, storage, realtime, los suyos), las apps que hablan con Postgres por conexión directa (psql, un ORM, un servidor con connection string) y la propia RLS. El cambio afecta a la Data API: REST, GraphQL y todas las librerías cliente construidas encima.
¿Qué se rompe y qué sigue funcionando?
Mi entorno: Supabase CLI 2.109.1, Postgres 17.6, PostgREST 14.14, el 26 de octubre de 2026. Puse auto_expose_new_tables = true en config.toml para tener los defaults antiguos, creé una tabla con dos filas, RLS activado y una policy de lectura, y ejecuté las dos sentencias de arriba. Después creé objetos nuevos y llamé a cada uno con la clave pública local (anon) y con la secret key (service_role), una petición por caso.
| Objeto, creado tras el cambio salvo indicación | Clave | Resultado |
|---|---|---|
| Tabla creada antes del cambio | pública | HTTP 200, 2 filas |
Tabla nueva con RLS y una policy to anon, authenticated |
pública | HTTP 401, 42501, hint GRANT SELECT ON public.new_orders TO anon; |
| La misma tabla nueva | secret | HTTP 403, 42501, hint … TO service_role; |
Vista nueva (security_invoker) sobre la tabla antigua |
pública | HTTP 401, 42501, permission denied for view, hint null |
Copia hecha con create table … (like old_orders including all) |
pública | HTTP 401, 42501 |
La tabla antigua tras drop table y create table de nuevo |
pública | HTTP 401, 42501 |
Función nueva public.ping() en /rest/v1/rpc |
pública | HTTP 200 |
Tabla nueva con id serial, solo select, insert para anon |
pública | insert HTTP 201 |
De esa tabla conviene saber cinco cosas antes del cambio.
“Las tablas existentes conservan sus grants” solo vale mientras la tabla exista. Una migración que borra y recrea una tabla, una copia con create table … (like …) o una restauración desde un dump crean una tabla nueva, y una tabla nueva no recibe nada. La policy volvió con mi create policy; los grants no. Todo lo que en su pipeline reconstruya tablas en lugar de alterarlas se rompe en la primera ejecución tras el cambio.
La secret key también falla. service_role se salta RLS, pero sigue necesitando un grant. Así que una Edge Function, un cron job o un webhook que escribe en una tabla nueva falla con 403, no solo el navegador. El hint nombra service_role, y así se distingue.
Las vistas no dan ningún hint. Para las tablas PostgREST devuelve el GRANT exacto que hace falta. En mi vista nueva el hint fue null y el mensaje decía permission denied for view v_old_orders. Una herramienta de IA que arregla el 42501 a partir del hint no tiene aquí nada que copiar; el grant es grant select on public.v_old_orders to authenticated;, y la vista debe mantener security_invoker = true para que la RLS de la tabla siga aplicándose.
Las funciones no entran en este cambio. Las dos sentencias tocan tablas y secuencias. Mi función nueva siguió siendo invocable por anon, authenticated y service_role, porque Postgres da execute a PUBLIC en toda función nueva y un revoke a nivel de esquema no puede quitarlo, como explica la guía de seguridad de la API de Supabase. Las llamadas RPC siguen funcionando después del 30 de octubre; también significa que una función security definer nueva se puede llamar con su clave pública desde el instante en que existe.
Los inserts con serial siguieron funcionando sin grant en la secuencia. Muchos consejos dicen que un id serial necesita grant usage on sequence … to authenticated tras el cambio. En mi prueba el insert pasó sin él: el revoke quita usage y select en las secuencias nuevas, pero update se queda, y update basta para nextval. Si revoca todos los default privileges de las secuencias, como hace el revoke completo de la guía de seguridad de la API, entonces sí hace falta el grant de usage. Una columna identity (generated by default as identity) no necesita nada en ningún caso.
Una cosa más que el revoke deja atrás: la tabla nueva seguía llegando con TRUNCATE, REFERENCES y TRIGGER para los tres roles, y en Postgres 17 los defaults incluyen además MAINTAIN. Ninguna policy RLS se aplica a TRUNCATE. Lo traté en la nota sobre 42501.
Dónde lo verá antes del 30 de octubre
El cambio en su proyecto alojado llega el día 30, pero hay tres lugares que ya se comportan como después.
supabase db reset con un CLI reciente. El CLI v2.102.0 añadió [api] auto_expose_new_tables a config.toml. En la 2.109.1, sin esa clave, mi stack nuevo ya tenía los defaults nuevos: las tablas recibían solo MAINTAIN, REFERENCES, TRIGGER y TRUNCATE, y las secuencias solo UPDATE. Con true vuelven los grants antiguos y se imprime api.auto_expose_new_tables is deprecated and will be removed on 2026-10-30. Así que si sus tablas locales siguen funcionando tras un reset, mire primero esa clave. Es también el sitio más barato para probar: quítela, haga el reset y ejecute su app.
Preview branches y proyectos creados desde el 30 de mayo. Reproducen sus migraciones con los defaults nuevos. Toda tabla cuya migración no tenga grant ya es inaccesible ahí. En la discusión de GitHub, quienes tienen un proyecto por cliente cuentan justo esto: los proyectos en marcha están bien, cada deploy nuevo con las mismas migraciones está roto.
Una migración baseline que trae de vuelta el default antiguo. Un comentarista de la misma discusión señala que una primera migración hecha con supabase db pull puede contener alter default privileges … grant all on tables to anon, authenticated, service_role. Ejecuté esa sentencia tras el cambio: la siguiente tabla que creé volvió a ser accesible, HTTP 200. En local eso oculta el problema. Aplicada a su proyecto alojado, deshace el cambio en silencio y toda tabla futura vuelve a estar abierta a la clave pública, tenga RLS o no. Busque alter default privileges en sus migraciones y lea cada resultado.
Cómo comprobar su proyecto antes del 30 de octubre
Cuatro checks, del más rápido al más completo. Los dos primeros se ejecutan en el SQL Editor del proyecto alojado.
1. ¿Ya cambió su proyecto?
Es la consulta de la guía de seguridad de la API de Supabase. Lista lo que recibirán los objetos nuevos creados por postgres en public:
select
case d.defaclobjtype
when 'r' then 'tables'
when 'S' then 'sequences'
when 'f' then 'functions'
end as object_type,
a.grantee::regrole as role,
string_agg(a.privilege_type, ', ' order by a.privilege_type) as privileges
from pg_default_acl d
cross join lateral aclexplode(d.defaclacl) a
where d.defaclrole = 'postgres'::regrole
and d.defaclnamespace = 'public'::regnamespace
and a.grantee in ('anon'::regrole, 'authenticated'::regrole, 'service_role'::regrole)
group by 1, 2
order by 1, 2;
Si las filas de tables contienen SELECT, INSERT, UPDATE, DELETE, el proyecto sigue con el default antiguo y cambia el día 30. Si solo muestran MAINTAIN, REFERENCES, TRIGGER, TRUNCATE (o no hay filas), las tablas nuevas ya están cerradas. También se ve en el Dashboard, en los ajustes de la Data API, “Automatically expose new tables”.
2. ¿Qué tablas tienen una policy pero ningún grant?
Una policy para un rol que no tiene grant para ese comando nunca se ejecuta: la petición falla antes de la RLS. Esta consulta lista esos pares. En mi laboratorio encontró la tabla nueva y la antigua recreada, y nada más:
select p.schemaname || '.' || p.tablename as "table", r.role, p.cmd
from pg_policies p
cross join lateral unnest(case when 'public' = any(p.roles) then array['anon', 'authenticated']::name[] else p.roles end) as r(role)
join pg_class c on c.relname = p.tablename
join pg_namespace n on n.oid = c.relnamespace and n.nspname = p.schemaname
where p.schemaname = 'public'
and r.role in ('anon', 'authenticated')
and not has_table_privilege(r.role::text, c.oid,
case p.cmd when 'ALL' then 'SELECT' else p.cmd end)
order by 1, 2, 3;
Ejecútela tras cada migración, en CI contra un db reset limpio o en staging. En un proyecto sano no devuelve nada; después del día 30 cualquier fila es una tabla a la que su app no llega. Comprueba grants a nivel de tabla, así que una tabla abierta solo con grants por columna (grant select (id, name) on …) aparece aquí aunque esas columnas se puedan leer.
3. ¿Qué migraciones crean una tabla sin grant?
La base de datos solo muestra las tablas que existen. Una migración que aún no ha aplicado solo se ve en su repo. Este grep aproximado lista cada create table de supabase/migrations sin un grant … on para el mismo nombre:
for t in $(grep -hioE 'create table( if not exists)? +[a-z0-9_."]+' supabase/migrations/*.sql | awk '{print $NF}' | sort -u); do
grep -qiF "on $t " supabase/migrations/*.sql || grep -qiF "on table $t " supabase/migrations/*.sql || echo "no grant: $t"
done
Es coincidencia de texto, no un parser de SQL: un grant que enumera varias tablas en una sentencia, o una tabla nombrada sin esquema en un sitio y con esquema en otro, da un falso positivo. Lea cada línea que imprime. Las tablas a las que solo llega el código de servidor por conexión directa no necesitan ningún grant.
4. Reproducirlo todo con los defaults nuevos
Quite auto_expose_new_tables de config.toml, ejecute supabase db reset y lance su app o sus tests contra eso. Es su proyecto después del 30, más una copia limpia de cada tabla, que es el caso más difícil. Supabase también lista las tablas afectadas en el Security Advisor y marca la exposición a la Data API en el Table Editor.
Cómo arreglarlo sin abrir todas las tablas
Escriba el grant, la RLS y las policies en la migración que crea la tabla, y dé a cada rol solo lo que usa la app:
create table public.invoices (
id bigint generated by default as identity primary key,
user_id uuid not null default auth.uid(),
total numeric not null
);
revoke all on table public.invoices from anon, authenticated, service_role;
grant select, insert on table public.invoices to authenticated;
grant select, insert, update, delete on table public.invoices to service_role;
alter table public.invoices enable row level security;
create policy "Owners read their invoices"
on public.invoices for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Owners add their invoices"
on public.invoices for insert
to authenticated
with check ((select auth.uid()) = user_id);
Ejecuté este bloque en el mismo stack local: un usuario de prueba con sesión insertó una fila y la leyó (HTTP 201, luego 200), la clave pública sin sesión obtuvo 42501, y el check 2 de arriba ya no listaba la tabla. El revoke all inicial limpia los TRUNCATE, TRIGGER y REFERENCES que acompañan a una tabla nueva, y da el mismo resultado en un proyecto que aún no ha cambiado. Dé anon solo sobre datos pensados para visitantes sin sesión, y service_role solo si el código de servidor usa la secret key en esta tabla. Cómo encajan grants y policies en una app entera está en mi checklist de seguridad para apps de Lovable.
Qué no hacer cuando llegue el primer 42501:
- No ejecute
grant all on all tables in schema public to anon, authenticated. Devuelve el default antiguo a todas las tablas de golpe, incluida cualquier tabla en la que nunca se activó RLS, y esas pasan a ser legibles por cualquiera que tenga su clave pública. La nota sobre 42501 explica por qué el grant es el primer cerrojo y la RLS el segundo. - No vuelva a añadir
alter default privileges … grant. Es el rollback de todo el cambio. - No desactive RLS para “probar” ni ponga la secret key en el frontend. La secret key se salta RLS para cualquiera que abra su bundle; la diferencia entre las claves está en anon key vs service role key.
Si una herramienta de IA escribe sus migraciones, añada una línea a sus instrucciones: “every new table in public gets explicit grants per role, enable row level security and policies in the same migration; never grant on all tables in schema public”. El hint del error le enseña el grant, nunca la RLS que debe ir con él.
Preguntas habituales sobre el cambio
¿Dejarán de funcionar mis tablas existentes el 30 de octubre? No. Los default privileges se aplican a objetos creados después, y en mi prueba la tabla antigua respondió HTTP 200 tras el cambio. La excepción es una tabla que sus migraciones borran y vuelven a crear.
¿Tengo que hacer algo si nunca creo tablas? No el día 30. Compruébelo la próxima vez que usted, un compañero o una herramienta de IA añada una, y revise cualquier pipeline que cree proyectos o branches a partir de sus migraciones.
¿Afecta a las Edge Functions? Sí, si usan supabase-js con la secret key sobre una tabla nueva: service_role también necesita un grant. Las funciones que se conectan directamente a Postgres no se ven afectadas.
¿Afecta a las funciones RPC, a Storage o a Auth? No en este cambio. Las funciones nuevas siguen siendo invocables vía PUBLIC, y storage y auth no son el esquema public.
¿Puedo hacer el cambio antes? Sí. Las dos sentencias del principio son el cambio; ejecútelas primero en staging y luego en producción. Las tablas existentes conservan sus grants, así que una app en marcha ni lo nota, y la siguiente tabla nueva le dice al instante si sus migraciones están listas.
Cómo lo ve el checker
El checker se conecta en solo lectura y lee los grants de cada tabla de sus esquemas expuestos: en qué tablas puede escribir la clave pública sin policy, qué tablas tienen RLS desactivado y qué default privileges siguen dando a cada tabla nueva TRUNCATE, REFERENCES o TRIGGER. También lista cada policy cuyo rol no tiene grant para ese comando, las tablas que encuentra el check 2, salvo las que tienen RLS desactivado o grants por columna. No puede ver archivos de migración que no haya aplicado, así que el check 3 de arriba lo ejecuta usted. Si la parte que le genera dudas es RLS, cómo probar Supabase RLS lo recorre paso a paso, y la nota sobre esquemas expuestos cubre los esquemas distintos de public, que este cambio no toca.