Buckets públicos y privados en Supabase y URLs firmadas
Cuándo un bucket de Supabase Storage debe ser público o privado, cómo funcionan las URLs firmadas, qué políticas necesitas y cómo corregir un bucket mal puesto.
Read in English →
En Supabase Storage, un bucket público deja que cualquiera que tenga la URL de un archivo lo descargue, sin iniciar sesión y sin revisar políticas. Un bucket privado revisa las políticas de Row Level Security en cada descarga, y los archivos se comparten con una descarga autenticada o con una URL firmada que vence. Usa buckets públicos solo para lo que no te molestaría ver en un cartel (logos, fotos de productos, avatares públicos). Todo lo que pertenece a una persona, como facturas, contratos, documentos de identidad y archivos subidos por usuarios, va en un bucket privado.
En esta guía verás qué controla realmente cada opción, las políticas que necesita un bucket privado, cómo usar URLs firmadas sin filtrarlas y cómo arreglar un bucket que se creó con la configuración equivocada.
¿Qué cambia exactamente al marcarlo como público?
Menos de lo que se suele creer. Según la documentación de buckets de Supabase, un bucket público solo se salta el control de acceso al leer y servir archivos. Subir, borrar, mover y copiar siguen dependiendo de tus políticas:
| Bucket público | Bucket privado (por defecto) | |
|---|---|---|
| Descargar | Cualquiera con la URL | Necesita una política o una URL firmada |
| Subir, borrar, mover | Necesita una política | Necesita una política |
| Duración de la URL | Para siempre | Las firmadas vencen |
| Uso típico | Logos, imágenes de productos, avatares | Documentos, recibos, archivos de usuarios |
La dirección de un archivo público sigue este formato, fácil de adivinar si conoces la ruta de un solo archivo:
https://<project-ref>.supabase.co/storage/v1/object/public/<bucket>/<ruta>
Si las rutas incluyen IDs de usuario o números consecutivos, quien vea una URL puede probar otras. Por eso "es público, pero nadie conoce el enlace" no protege nada.
Supabase también indica que los buckets públicos rinden mejor porque se almacenan en caché de otra manera: un motivo más para dejar ahí solo lo que de verdad es público.
¿Cómo sé cuáles de mis buckets son públicos?
En el panel, entra a Storage y revisa la configuración de cada bucket; los públicos aparecen marcados. O ejecuta esto en el editor SQL:
select id, name, public, file_size_limit, allowed_mime_types
from storage.buckets;
En cada bucket que diga true, abre algunos archivos y pregúntate: ¿me molestaría que un desconocido viera esto? Las herramientas de IA suelen crear un único bucket público llamado algo como uploads y meter todo ahí.
¿Qué políticas necesita un bucket privado?
El acceso a Storage pasa por RLS en la tabla storage.objects. Una estructura común y segura es una carpeta por usuario, con su ID como nombre, por ejemplo facturas/2f1c.../marzo.pdf. Entonces:
create policy "Cada usuario sube a su carpeta"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'facturas'
and (storage.foldername(name))[1] = (select auth.jwt() ->> 'sub')
);
create policy "Cada usuario lee sus archivos"
on storage.objects for select
to authenticated
using (
bucket_id = 'facturas'
and (storage.foldername(name))[1] = (select auth.jwt() ->> 'sub')
);
storage.foldername(name) separa la ruta en carpetas y [1] es la primera. Agrega políticas de update y delete solo si los usuarios deben poder reemplazar o borrar archivos. La guía de control de acceso de Storage aclara que una subida con "upsert" (sobrescribir si ya existe) necesita permisos de select y update además de insert.
Para archivos compartidos en un equipo, usa el ID de la organización como primera carpeta y verifica la membresía en lugar del ID del usuario; en la guía multi-empresa está la función auxiliar.
¿Cómo funcionan las URLs firmadas?
Una URL firmada es un enlace normal con un token que vence. Quien la tenga puede descargar el archivo hasta ese momento, sin iniciar sesión. Se crea desde código que tiene permiso para leer el archivo:
const { data, error } = await supabase.storage
.from('facturas')
.createSignedUrl(`${userId}/marzo.pdf`, 60 * 10); // vence en 10 minutos
El segundo argumento es la duración en segundos. Algunas reglas prácticas:
- Usa duraciones cortas. Minutos para un botón de descarga, no días. Genera un enlace nuevo cada vez que el usuario haga clic.
- No guardes URLs firmadas en la base de datos. Guarda la ruta del archivo y fírmala cuando haga falta. Un enlace guardado o vence y se rompe, o dura tanto que en la práctica es público.
- Créalas donde se hace la verificación. Si el enlace se crea con la sesión del propio usuario, RLS decide si puede. Si lo crea un servidor con la clave secret, el servidor debe comprobar antes quién lo pide, porque esa clave se salta RLS.
- Trátala como una contraseña temporal. No la pongas en correos que se reenvían ni en eventos de analítica o logs.
Para subidas desde el navegador sin abrir permisos de escritura de forma amplia, Supabase también ofrece URLs firmadas de subida (createSignedUploadUrl), que permiten una subida a una ruta concreta.
¿Conviene limitar tamaño y tipo de archivo?
Sí. Cada bucket puede tener su tamaño máximo y su lista de tipos MIME permitidos; un bucket de avatares, por ejemplo, puede aceptar solo image/png, image/jpeg e image/webp de pocos megas. Se configura en los ajustes del bucket o al crearlo. Así evitas que alguien use tu almacenamiento para alojar archivos enormes y reduces el riesgo de que se sirvan archivos HTML o scripts desde tu dominio. Si lo que falla son las subidas, revisa subir archivos a Supabase Storage desde Lovable.
¿Cómo arreglo un bucket que debía ser privado?
- Revisa qué contiene y decide qué archivos son privados.
- Crea un bucket privado nuevo (o cambia el actual a privado) y agrega las políticas de
storage.objectsde arriba. - Cambia el código que arma las URLs: reemplaza
getPublicUrlporcreateSignedUrlo por descargas autenticadas. - Actualiza las URLs guardadas. Si tus tablas guardan URLs públicas completas, guarda rutas y fírmalas al momento.
- Cambia la opción y prueba con un navegador sin sesión: el enlace público viejo ya no debería devolver el archivo.
- Piensa en quién pudo ver qué. Si hubo documentos personales en un bucket público, evalúa si es una exposición de datos que debes atender. Esto no es asesoría legal; si hay datos personales de por medio, consulta con un abogado.
Deja los archivos públicos, como logos, en un bucket público aparte, para no mezclar nunca los dos tipos.
Respuestas rápidas
¿Puedo hacer público un solo archivo dentro de un bucket privado? No con una opción. Dale una URL firmada de larga duración o, mejor, muévelo a un bucket público.
¿Un bucket público deja listar todos los archivos? Listar pasa por la API y necesita una política de select en storage.objects; descargar por URL, no. No agregues políticas de select amplias a buckets públicos.
¿Los archivos de Storage entran en los respaldos de Supabase? No. Los respaldos guardan los metadatos de los archivos, no los archivos. Lo explicamos en respaldos de Supabase.
¿Por qué mi imagen privada no aparece en una etiqueta <img>? El navegador la pide sin tu token de sesión. Usa una URL firmada como src.
¿Quieres que revisemos tus buckets?
La revisión gratuita de tu app muestra lo que tu app en producción deja ver desde afuera. Una revisión de seguridad de Supabase recorre cada bucket, cada política y cada lugar del código donde se arman URLs. También puedes hacernos una consulta sobre tu configuración.


