Storage en Supabase: Buckets y Permisos
Supabase Storage es un servicio de almacenamiento de archivos construido sobre S3 de AWS. Te permite subir, descargar y gestionar archivos (imágenes, PDFs, videos, etc.) con un sistema de permisos similar a RLS.
La arquitectura es simple:
Storage
└── Buckets (contenedores)
└── Carpetas (opcionales)
└── ArchivosCada bucket es un contenedor independiente con su propia configuración de permisos, límites de tamaño y tipos de archivo permitidos.
Buckets públicos vs privados
La diferencia principal entre un bucket público y uno privado es cómo se accede a los archivos.
| Bucket público | Bucket privado | |
|---|---|---|
| URL de acceso | URL pública directa | URL firmada (signed URL) con expiración |
| Autenticación | No requiere | Requiere token |
| Uso típico | Avatares, imágenes de productos | Documentos privados, facturas, archivos de usuario |
| Cache CDN | Si | No (las URLs expiran) |
Bucket público
Cualquier persona con la URL puede acceder al archivo. No se necesita autenticación para leer.
https://tu-proyecto.supabase.co/storage/v1/object/public/avatares/foto.jpgUsa buckets públicos para contenido que no es sensible: logos, imágenes de productos, assets de tu sitio.
Bucket privado
Los archivos solo son accesibles con una URL firmada (signed URL) que tiene una fecha de expiración. Esto es útil para documentos que solo ciertos usuarios deben ver.
https://tu-proyecto.supabase.co/storage/v1/object/sign/documentos/factura.pdf?token=eyJ...&expires_in=3600La URL deja de funcionar cuando expira el token.
Crear buckets
Desde el dashboard
- Ve a Storage en el dashboard de Supabase
- Click en New bucket
- Ingresa el nombre del bucket
- Selecciona si es público o privado
- Configura límites (opcional)
- Click en Create bucket
Con SQL
-- Crear bucket publico para avatares
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatares', 'avatares', true);
-- Crear bucket privado para documentos
INSERT INTO storage.buckets (id, name, public)
VALUES ('documentos', 'documentos', false);Con el SDK
// Crear bucket publico
const { data, error } = await supabase.storage.createBucket('avatares', {
public: true,
fileSizeLimit: 1024 * 1024 * 2, // 2MB
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp']
})
// Crear bucket privado
const { data: docBucket, error: docError } = await supabase.storage.createBucket('documentos', {
public: false,
fileSizeLimit: 1024 * 1024 * 10, // 10MB
allowedMimeTypes: ['application/pdf']
})Nombres de buckets
Usa nombres en minusculas, sin espacios, separados por guiones: fotos-perfil, documentos-legales, assets-públicos. Los nombres de buckets no se pueden cambiar después de creados.
Configurar límites de archivos
Tamaño máximo
// Al crear el bucket
const { data } = await supabase.storage.createBucket('imagenes', {
public: true,
fileSizeLimit: 1024 * 1024 * 5 // 5MB en bytes
})
// Actualizar un bucket existente
const { data: updated } = await supabase.storage.updateBucket('imagenes', {
fileSizeLimit: 1024 * 1024 * 10 // Cambiar a 10MB
})Si un usuario intenta subir un archivo que excede el límite, la operación falla con un error.
Tipos MIME permitidos
Restringe que tipos de archivos se pueden subir:
// Solo imagenes
const { data } = await supabase.storage.createBucket('fotos', {
public: true,
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp']
})
// Solo PDFs
const { data: docs } = await supabase.storage.createBucket('facturas', {
public: false,
allowedMimeTypes: ['application/pdf']
})
// Imagenes y videos
const { data: media } = await supabase.storage.createBucket('media', {
public: true,
allowedMimeTypes: ['image/*', 'video/*']
})Validación de MIME types
La validación de MIME types se hace del lado del servidor. Aun así, siempre valida también en el cliente antes de subir para dar mejor feedback al usuario. Un usuario malintencionado puede cambiar el MIME type del request, pero Supabase también verifica el contenido del archivo.
Políticas de Storage
Las políticas de Storage funcionan de manera similar a las políticas RLS de tablas. Cada bucket necesita políticas para controlar quien puede subir, leer, actualizar y eliminar archivos.
Las operaciones de Storage se mapean a tablas internas de Supabase:
| Operación | Tabla | Descripción |
|---|---|---|
| Subir archivo | storage.objects INSERT | Crear un archivo nuevo |
| Leer archivo | storage.objects SELECT | Descargar o ver un archivo |
| Actualizar archivo | storage.objects UPDATE | Reemplazar un archivo existente |
| Eliminar archivo | storage.objects DELETE | Eliminar un archivo |
Política: cualquiera puede leer archivos públicos
Para un bucket público, necesitas una política de SELECT:
CREATE POLICY "lectura pública de avatares"
ON storage.objects FOR SELECT
TO anon, authenticated
USING (bucket_id = 'avatares');Política: usuarios autenticados suben a su carpeta
El patrón más común: cada usuario tiene su propia carpeta dentro del bucket, identificada por su UUID.
-- Estructura: avatares/{user_id}/archivo.jpg
CREATE POLICY "usuarios suben su avatar"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'avatares'
AND (storage.foldername(name))[1] = auth.uid()::text
);storage.foldername(name) retorna un array con los segmentos de la ruta. El primer segmento [1] es la carpeta del usuario.
Política: usuarios leen sus propios archivos privados
CREATE POLICY "usuarios leen sus documentos"
ON storage.objects FOR SELECT
TO authenticated
USING (
bucket_id = 'documentos'
AND (storage.foldername(name))[1] = auth.uid()::text
);Política: usuarios eliminan sus propios archivos
CREATE POLICY "usuarios eliminan sus archivos"
ON storage.objects FOR DELETE
TO authenticated
USING (
bucket_id = 'avatares'
AND (storage.foldername(name))[1] = auth.uid()::text
);Set completo de políticas para un bucket de avatares
-- Lectura pública (cualquiera puede ver avatares)
CREATE POLICY "avatares lectura pública"
ON storage.objects FOR SELECT
TO anon, authenticated
USING (bucket_id = 'avatares');
-- Solo el dueno puede subir a su carpeta
CREATE POLICY "avatares subir propio"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'avatares'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- Solo el dueno puede reemplazar su avatar
CREATE POLICY "avatares actualizar propio"
ON storage.objects FOR UPDATE
TO authenticated
USING (
bucket_id = 'avatares'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- Solo el dueno puede eliminar su avatar
CREATE POLICY "avatares eliminar propio"
ON storage.objects FOR DELETE
TO authenticated
USING (
bucket_id = 'avatares'
AND (storage.foldername(name))[1] = auth.uid()::text
);Crear políticas desde el dashboard
También puedes crear políticas de Storage desde el dashboard en Storage > Policies. Supabase ofrece templates para los patrones más comunes.
URLs públicas vs URLs firmadas
URL pública
Para archivos en buckets públicos. No expira, cualquiera puede acceder:
const { data } = supabase.storage
.from('avatares')
.getPublicUrl('user-123/foto.jpg')
console.log(data.publicUrl)
// https://tu-proyecto.supabase.co/storage/v1/object/public/avatares/user-123/foto.jpgUsa está URL directamente en tags <img> o en cualquier lugar donde necesites mostrar la imagen.
URL firmada (signed URL)
Para archivos en buckets privados. Requiere autenticación y tiene fecha de expiración:
const { data, error } = await supabase.storage
.from('documentos')
.createSignedUrl('user-123/factura.pdf', 3600) // expira en 3600 segundos (1 hora)
if (data) {
console.log(data.signedUrl)
// https://tu-proyecto.supabase.co/storage/v1/object/sign/documentos/user-123/factura.pdf?token=eyJ...
}Multiples URLs firmadas de una vez
const { data, error } = await supabase.storage
.from('documentos')
.createSignedUrls(
['user-123/factura-01.pdf', 'user-123/factura-02.pdf', 'user-123/factura-03.pdf'],
3600
)
// data es un array de { signedUrl, path, error }Cuando usar cada tipo de URL
Usa URLs públicas para contenido que no es sensible y que quieres que sea cacheable por CDN (avatares, imágenes de productos). Usa URLs firmadas para contenido privado que solo ciertos usuarios deben ver (documentos, reportes, archivos personales).
Listar archivos en un bucket
// Listar archivos en la raiz del bucket
const { data: archivos, error } = await supabase.storage
.from('avatares')
.list()
// Listar archivos en una carpeta especifica
const { data: misArchivos, error: listError } = await supabase.storage
.from('documentos')
.list('user-123', {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' }
})
// Cada archivo tiene: name, id, created_at, updated_at, metadataMover y copiar archivos
// Mover un archivo (renombrar)
const { data, error } = await supabase.storage
.from('documentos')
.move('user-123/viejo-nombre.pdf', 'user-123/nuevo-nombre.pdf')
// Copiar un archivo
const { data: copia, error: copyError } = await supabase.storage
.from('documentos')
.copy('user-123/original.pdf', 'user-123/copia.pdf')Listar y administrar buckets
// Listar todos los buckets
const { data: buckets, error } = await supabase.storage.listBuckets()
// Obtener info de un bucket
const { data: bucket, error: getError } = await supabase.storage.getBucket('avatares')
// Actualizar configuracion de un bucket
const { data: updated, error: updateError } = await supabase.storage.updateBucket('avatares', {
public: true,
fileSizeLimit: 1024 * 1024 * 5
})
// Vaciar un bucket (eliminar todos los archivos)
const { data: emptied, error: emptyError } = await supabase.storage.emptyBucket('temp')
// Eliminar un bucket (debe estar vacío)
const { data: deleted, error: deleteError } = await supabase.storage.deleteBucket('temp')Resumen
- Supabase Storage usa buckets como contenedores de archivos
- Los buckets públicos sirven contenido sin autenticación, los privados requieren URLs firmadas
- Las políticas de Storage se crean en
storage.objectscon la misma sintaxis que RLS - Usa
storage.foldername(name)para crear políticas basadas en carpetas por usuario - Configura límites de tamaño y tipos MIME al crear el bucket
- URLs públicas son para contenido no sensible, URLs firmadas para contenido privado