Saltar al contenido principal

Supabase Storage

Almacenamiento de archivos integrado en Supabase. Ideal si ya usas Supabase como base de datos, ya que todo está en el mismo proyecto.

Límites del plan gratuito

ParámetroValor
Storage1 GB
Transferencia (bandwidth)2 GB/mes
Tamaño máximo por archivo50 MB (free)
Transformación de imágenes✅ Incluida (resize, crop, format)

Crear bucket

Supabase Dashboard → Storage → New bucket
→ Name: avatares (o el nombre que necesites)
→ Public: ✅ si los archivos son públicos (imágenes de perfil, etc.)
❌ si son privados (documentos privados)
→ File size limit: en bytes (ej: 52428800 = 50 MB)
→ Allowed MIME types: image/jpeg, image/png, image/webp (opcional)

Subir archivos desde JavaScript

import { supabase } from '@/lib/supabase'

// Subir desde un input de archivo (browser)
async function subirAvatar(userId: string, file: File) {
const extension = file.name.split('.').pop()
const filePath = `${userId}/avatar.${extension}`

const { data, error } = await supabase.storage
.from('avatares')
.upload(filePath, file, {
contentType: file.type,
upsert: true // sobreescribir si ya existe
})

if (error) throw error
return data.path
}

// Subir desde buffer/base64 (Node.js)
import fs from 'fs'

const buffer = fs.readFileSync('./imagen.jpg')
const { data, error } = await supabase.storage
.from('documentos')
.upload('reporte-2024.pdf', buffer, {
contentType: 'application/pdf'
})

Obtener URLs

// URL pública (solo funciona si el bucket es público)
const { data } = supabase.storage
.from('avatares')
.getPublicUrl('usuario-123/avatar.jpg')

console.log(data.publicUrl)
// https://xxxx.supabase.co/storage/v1/object/public/avatares/usuario-123/avatar.jpg

// URL firmada (para archivos privados, válida por tiempo limitado)
const { data, error } = await supabase.storage
.from('documentos')
.createSignedUrl('reporte-privado.pdf', 3600) // válida 1 hora

console.log(data?.signedUrl)

// Múltiples URLs firmadas
const { data } = await supabase.storage
.from('documentos')
.createSignedUrls(['archivo1.pdf', 'archivo2.pdf'], 3600)

Transformación de imágenes (Supabase Image Transformation)

// Redimensionar imagen automáticamente
const { data } = supabase.storage
.from('avatares')
.getPublicUrl('usuario-123/avatar.jpg', {
transform: {
width: 200,
height: 200,
resize: 'cover', // cover, contain, fill
format: 'webp', // convertir a WebP automáticamente
quality: 80 // calidad (1-100)
}
})

Listar archivos

// Listar archivos en un "directorio" (prefijo)
const { data, error } = await supabase.storage
.from('avatares')
.list('usuario-123/', {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' }
})

data?.forEach(file => {
console.log(file.name, file.metadata?.size)
})

Eliminar archivos

// Eliminar uno o varios archivos
const { error } = await supabase.storage
.from('avatares')
.remove(['usuario-123/avatar.jpg', 'usuario-456/avatar.png'])

Políticas de seguridad (RLS para Storage)

-- Solo el dueño puede subir a su carpeta
CREATE POLICY "upload_propio" ON storage.objects
FOR INSERT WITH CHECK (
bucket_id = 'avatares'
AND auth.uid()::text = (storage.foldername(name))[1]
);

-- Solo el dueño puede ver sus archivos (privados)
CREATE POLICY "ver_propio" ON storage.objects
FOR SELECT USING (
bucket_id = 'documentos'
AND auth.uid()::text = (storage.foldername(name))[1]
);

-- Lectura pública para el bucket de imágenes
CREATE POLICY "lectura_publica" ON storage.objects
FOR SELECT USING (bucket_id = 'publico');

⚠️ Limitaciones importantes

  • 1 GB total: se llena rápido con videos o muchas imágenes
  • 2 GB bandwidth/mes: puede ser insuficiente para apps con muchos archivos
  • 50 MB por archivo: no apto para videos largos (usar Cloudflare R2 o Backblaze B2)
  • Ligado al proyecto Supabase: si pausas el proyecto, el storage también se pausa