Supabase Storage na prática: upload de arquivos, CDN e controle de acesso

Na semana passada, um leitor me perguntou: “Para permitir que usuários enviem avatares, é melhor usar S3 ou Cloudflare R2?”
Parei para pensar. Já tive problemas com isso em dois projetos diferentes. Configurar as políticas de permissão do S3 quase me fez perder a cabeça; o R2 é barato, mas exige montar um sistema de autenticação por conta própria. Mais tarde, migrei os projetos para o Supabase Storage — não porque ele seja uma “bala de prata”, mas porque, se você já usa o Auth e o banco de dados do Supabase, o conjunto funciona muito bem.
Neste artigo, vou destrinchar os mecanismos centrais do Supabase Storage, três modelos de controle de acesso, as armadilhas do upload de arquivos grandes, técnicas de otimização da CDN e uma comparação de custos com R2 e S3. Todo o código pode ser executado diretamente.
1. Arquitetura central do Supabase Storage
Primeiro, é importante deixar uma coisa clara: por baixo dos panos, o Supabase Storage usa o AWS S3.
Porém, ele adiciona uma camada de abstração muito fina — fina o bastante para você operar tudo diretamente pelo SDK JavaScript, sem precisar lidar com o complexo sistema de credenciais e políticas IAM da AWS.
Integração automática com o Auth
Esse é o ponto de que mais gosto. Você cria um bucket no Supabase e pode usar diretamente o token JWT obtido por supabase.auth.getUser() para controlar quem pode fazer upload ou download. Não é preciso construir outro sistema de permissões.
// A identidade do usuário é enviada automaticamente no upload
const { data, error } = await supabase.storage
.from('avatars')
.upload('user-123/profile.jpg', file)
Por baixo dos panos, o serviço verifica suas políticas RLS (Row Level Security). Mais adiante, veremos em detalhes como configurá-las.
Distribuição automática por uma CDN global
Os arquivos enviados são distribuídos automaticamente pela CDN da Cloudflare. Você não precisa configurar CloudFront nem Cloudflare Workers por conta própria.
Abra as ferramentas de desenvolvimento do navegador e procure o header cf-cache-status na resposta:
cf-cache-status: HIT
HIT indica que o cache foi aproveitado; MISS, que não foi. Em geral, a taxa de acerto de um bucket público é bem maior que a de um bucket privado, porque este adota uma política de cache mais restrita.
Smart CDN: invalidação “automática” do cache
Esse é um dos destaques do Supabase. Em uma CDN tradicional, depois de atualizar um arquivo você precisa limpar o cache manualmente ou aguardar o TTL expirar. O Smart CDN do Supabase sincroniza automaticamente os metadados do arquivo com os pontos de presença. Quando o arquivo muda, a nova versão chega ao mundo todo em até 60 segundos.
Mas não comemore cedo demais: 60 segundos ainda é muito em cenários que exigem atualização em tempo real. Se você precisa que a mudança apareça imediatamente, terá de usar o parâmetro cacheNonce, apresentado mais adiante.
2. Comparação entre três modelos de controle de acesso
Muita gente se confunde aqui. Bucket público, bucket privado e Signed URL são três modelos diferentes, cada um indicado para determinados cenários. Uma escolha errada pode derrubar a taxa de acerto do cache ou até criar um problema de segurança.
2.1 Bucket público: a primeira opção para recursos abertos
Se os arquivos foram feitos para qualquer pessoa acessar — logo do site, imagens de artigos ou documentos públicos — use diretamente um bucket público.
Vantagens:
- URL mais simples:
https://xxx.supabase.co/storage/v1/object/public/bucket-name/file.jpg - Maior taxa de acerto do cache, pois a CDN responde diretamente, sem passar por uma validação do Auth
- Código simples, resolvido em uma única chamada
// Obtém a URL pública
const { data } = supabase.storage
.from('public-images')
.getPublicUrl('hero-banner.jpg')
console.log(data.publicUrl)
// https://xxx.supabase.co/storage/v1/object/public/public-images/hero-banner.jpg
Cenários indicados:
- Avatares de usuários exibidos publicamente
- Imagens de artigos
- Recursos estáticos do site
- Documentos públicos
2.2 Bucket privado + Signed URL: solução padrão para arquivos particulares
Alguns arquivos não podem ser públicos, como contratos enviados por usuários, conteúdo exclusivo para assinantes e documentos confidenciais. Nesse caso, use um bucket privado e gere uma Signed URL com prazo de validade.
// Gera um link de acesso válido por uma hora
const { data, error } = await supabase.storage
.from('private-docs')
.createSignedUrl('contracts/user-123.pdf', 3600) // 3600 segundos = 1 hora
console.log(data.signedUrl)
// https://xxx.supabase.co/storage/v1/object/sign/private-docs/contracts/user-123.pdf?token=xxx
Atenção: cada Signed URL gerada é diferente, o que afeta a taxa de acerto do cache. Se você gerar uma nova Signed URL a cada acesso, a CDN continuará respondendo com MISS.
Dica de otimização: quando o mesmo usuário acessa o mesmo arquivo várias vezes em um curto período, armazene a Signed URL no frontend ou no Redis, em vez de gerá-la novamente a cada solicitação.
2.3 Políticas RLS: controle de permissões granular
Esta é a parte mais poderosa e, ao mesmo tempo, mais ignorada. Você pode definir políticas RLS na tabela storage.objects para controlar com precisão quem pode operar cada arquivo.
Cenário 1: o usuário só pode enviar arquivos para a própria pasta
-- Cria uma política na tabela storage.objects
CREATE POLICY "Users can upload to own folder"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars'
AND auth.uid()::text = (storage.foldername(name))[1]
);
-- Explicação: name é o caminho completo do arquivo, como 'user-123/avatar.jpg'
-- storage.foldername(name)[1] obtém o nome da primeira pasta, ou seja, 'user-123'
-- auth.uid() é o ID do usuário conectado
-- O upload só é permitido quando os dois valores correspondem
Cenário 2: administradores podem acessar todos os arquivos
CREATE POLICY "Admins can access all"
ON storage.objects FOR ALL
USING (
auth.jwt() ->> 'role' = 'admin'
);
Cenário 3: somente assinantes podem baixar determinado conteúdo
CREATE POLICY "Members can download premium content"
ON storage.objects FOR SELECT
USING (
bucket_id = 'premium-content'
AND EXISTS (
SELECT 1 FROM user_subscriptions
WHERE user_id = auth.uid()
AND status = 'active'
)
);
Combinadas, essas três políticas atendem à maioria dos cenários de negócio.
3. Upload de arquivos na prática
Finalmente, vamos colocar a mão na massa.
3.1 Upload padrão: envie arquivos pequenos rapidamente
Para arquivos menores que 5 MB, basta usar o método upload.
// Componente de upload em React
import { useState } from 'react'
import { supabase } from './supabase-client'
export function AvatarUpload() {
const [uploading, setUploading] = useState(false)
const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setUploading(true)
const fileExt = file.name.split('.').pop()
const fileName = `${Date.now()}.${fileExt}`
const filePath = `avatars/${fileName}`
const { error } = await supabase.storage
.from('public-images')
.upload(filePath, file, {
cacheControl: '3600', // Cache do navegador por uma hora
upsert: false // Não sobrescreve um arquivo existente
})
if (error) {
alert('Falha no upload: ' + error.message)
} else {
const { data } = supabase.storage
.from('public-images')
.getPublicUrl(filePath)
setAvatarUrl(data.publicUrl)
}
setUploading(false)
}
return (
<div>
<input
type="file"
accept="image/*"
onChange={handleUpload}
disabled={uploading}
/>
{avatarUrl && <img src={avatarUrl} alt="avatar" />}
{uploading && <p>Enviando...</p>}
</div>
)
}
Alguns detalhes:
cacheControldefine o tempo de cache do navegador, que não é a mesma coisa que o cache da CDNupsert: falseimpede uma substituição acidental; para sobrescrever, mude o valor paratrue- O timestamp no nome evita duplicidades; você também pode usar um UUID
3.2 Upload em partes com TUS: uma solução estável para arquivos grandes
Para enviar arquivos com mais de 5 MB ou trabalhar em uma rede instável, use o upload em partes com TUS.
Limitação essencial: o chunkSize precisa ser de 6 MB e não pode ser alterado. Essa é uma limitação fixa do Supabase.
Validade: a URL de upload é válida por 24 horas. Depois disso, será preciso gerar outra.
Primeiro, instale as dependências:
npm install tus-js-client uppy @uppy/core @uppy/dashboard @uppy/tus
Código completo:
import Uppy from '@uppy/core'
import { Dashboard } from '@uppy/react'
import Tus from '@uppy/tus'
import { supabase } from './supabase-client'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
export function LargeFileUploader() {
const uppy = new Uppy({
restrictions: {
maxFileSize: 100 * 1024 * 1024, // 100 MB
allowedFileTypes: ['video/*', 'image/*']
}
})
// Obtém o token da sessão do Supabase
const getSession = async () => {
const { data: { session } } = await supabase.auth.getSession()
return session?.access_token || ''
}
uppy.use(Tus, {
endpoint: 'https://xxx.supabase.co/storage/v1/upload/resumable',
chunkSize: 6 * 1024 * 1024, // Deve ser 6 MB
async onBeforeRequest(req) {
const token = await getSession()
req.setHeader('Authorization', `Bearer ${token}`)
},
onAfterResponse(req, res) {
// Obtém o caminho do arquivo após a conclusão do upload
const location = res.getHeader('Location')
console.log('File uploaded to:', location)
}
})
return (
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<Dashboard uppy={uppy} />
</div>
)
}
Solução de problemas: se o upload em partes travar aos 6 MB, confira:
- Se o
chunkSizeé de 6 MB, exatamente - Se o token expirou; a validade é de 24 horas
- Se a política RLS permite INSERT; consulte a GitHub Issue #563
3.3 Presigned Upload URL: autorização para uploads de terceiros
Às vezes, você precisa permitir o upload direto pelo usuário sem expor a chave service_role. Use createSignedUploadUrl para gerar uma URL de upload pré-assinada.
// O servidor gera a URL de upload
const { data, error } = await supabase.storage
.from('user-uploads')
.createSignedUploadUrl('documents/report.pdf')
// data.signedUrl pode ser enviada ao frontend para fazer o upload diretamente
// O frontend não precisa conhecer a chave service_role
4. CDN e otimização de imagens
4.1 Como o Smart CDN funciona
Como vimos, o Smart CDN invalida o cache automaticamente depois que um arquivo é atualizado. Ainda assim, o atraso de até 60 segundos na propagação pode incomodar.
Boas práticas:
- Envie arquivos atualizados com frequência para um novo caminho
// Evite: atualizar sempre o mesmo arquivo
await storage.from('images').upload('logo.png', file, { upsert: true })
// Prefira: gerar um novo nome a cada atualização
const version = Date.now()
await storage.from('images').upload(`logo-${version}.png`, file)
- Use cacheNonce para ignorar o cache
const { data } = supabase.storage
.from('images')
.getPublicUrl('logo.png', {
cacheNonce: Date.now().toString() // Cada solicitação usa um valor diferente
})
- Reutilize a Signed URL
Quando o mesmo usuário acessa o mesmo arquivo, armazene a Signed URL e evite gerar outra a cada vez.
4.2 Transformação e otimização automática de imagens
O Supabase permite transformar imagens em tempo real: você pode ajustar largura, altura, qualidade e formato.
Limites:
- Largura e altura: 1–2500 px
- Tamanho do arquivo: ≤25 MB
- Resolução: ≤50 MP
// Obtém uma miniatura
const { data } = supabase.storage
.from('images')
.getPublicUrl('hero.jpg', {
transform: {
width: 300,
height: 200,
resize: 'cover', // Ou 'contain' ou 'fill'
quality: 80,
format: 'webp' // Converte automaticamente para WebP
}
})
Integração com o Image Loader do Next.js:
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './lib/supabase-image-loader.js'
}
}
// lib/supabase-image-loader.js
export default function supabaseLoader({ src, width, quality }) {
const url = new URL(src)
url.searchParams.set('width', width.toString())
url.searchParams.set('quality', (quality || 75).toString())
url.searchParams.set('format', 'webp')
return url.toString()
}
Preço: as transformações de imagem custam US$ 5 por 1.000 imagens de origem. Se suas imagens forem transformadas em muitas versões, como em vários tamanhos responsivos, inclua esse valor na conta.
5. Comparação de custos e recomendações de escolha
Muita gente se preocupa com esta parte. Vamos comparar as principais opções.
5.1 Tabela de preços
| Serviço | Armazenamento | Taxa de saída | Franquia gratuita | Destaques |
|---|---|---|---|---|
| Supabase Storage | Baseado nos preços do S3 | CDN cobrada à parte | Incluída no plano Pro | Integração com Auth e RLS |
| Cloudflare R2 | US$ 0,015/GB | Zero | 10 GB + 1 milhão de operações | Sem taxa de saída |
| AWS S3 | US$ 0,023/GB | US$ 0,09/GB | 5 GB por 12 meses | Ecossistema mais completo |
| DigitalOcean Spaces | US$ 5/250 GB | Incluída | Nenhuma | Preço fixo |
5.2 Como escolher
Muitos downloads → R2
Se seus arquivos serão baixados com frequência, como em um site de compartilhamento de imagens ou hospedagem de vídeos, a taxa de saída zero do R2 pode gerar uma grande economia. No S3, a saída custa quase dez centavos de dólar por GB, o que pesa bastante quando o tráfego aumenta.
Integração com Auth → Supabase Storage
Se você já usa o Auth e o banco de dados do Supabase, a integração com o Storage é muito simples. O controle de permissões dos usuários e as políticas RLS podem ser reutilizados diretamente.
Uso intenso do ecossistema AWS → S3
Lambda, CloudFront, S3 Select, S3 Glacier: se sua arquitetura já está vinculada à AWS, o custo da migração pode superar a economia obtida com outra solução.
Orçamento fixo e tráfego previsível → DigitalOcean Spaces
O preço mensal fixo é adequado para projetos pequenos que não querem se preocupar com cobrança por uso.
5.3 Recomendações para reduzir custos
- Políticas de ciclo de vida: arquive arquivos antigos automaticamente no Glacier
- Compressão de imagens: comprima antes do upload ou use as transformações de imagem do Supabase
- Buckets públicos para melhorar a taxa de acerto do cache: mantenha público tudo o que puder ser aberto
- Reutilização de Signed URLs: reduza a quantidade de URLs geradas e evite MISS recorrentes no cache
6. Solução de problemas comuns
6.1 A versão antiga continua aparecendo depois da atualização
Causa: atraso de até 60 segundos na propagação do Smart CDN.
Soluções:
- Aguarde 60 segundos
- Envie o arquivo para um novo caminho
- Use
cacheNoncepara ignorar o cache
6.2 O upload em partes trava aos 6 MB
Causa: configuração incorreta do chunkSize.
Solução: defina exatamente chunkSize: 6 * 1024 * 1024, até o último byte.
// Errado: definido como 5 MB
chunkSize: 5 * 1024 * 1024 // O upload ficará travado
// Correto: deve ser 6 MB
chunkSize: 6 * 1024 * 1024
6.3 O upload retorna 403 Forbidden
Causa: política RLS configurada incorretamente.
Etapas de verificação:
- Confira se o bucket é Public ou Private
- Confira as políticas RLS da tabela
storage.objects - Verifique se a política permite a operação INSERT
-- Lista as políticas existentes
SELECT * FROM pg_policies WHERE tablename = 'objects';
-- Adiciona uma política que permite uploads
CREATE POLICY "Allow upload"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'your-bucket');
6.4 A Signed URL não pode ser acessada
Causa: a URL expirou ou o token é inválido.
Soluções:
- Confira se o prazo de validade é adequado
- Verifique se o token não foi cortado
- Durante os testes, gere uma URL com validade longa, como 24 horas
Resumo
Recapitulando os principais pontos:
- Buckets públicos são ideais para recursos abertos e oferecem a melhor taxa de acerto do cache
- Buckets privados + Signed URL são indicados para arquivos particulares; preste atenção à estratégia de cache
- Políticas RLS oferecem controle de permissões granular e não devem ser ignoradas
- Uploads em partes com TUS são apropriados para arquivos grandes, e o chunkSize precisa ser de 6 MB
- Smart CDN invalida o cache automaticamente, mas há um atraso de até 60 segundos
- Escolha: R2 para muitos downloads, Supabase Storage para integração com Auth e S3 para quem já usa o ecossistema AWS
Se você já usa o banco de dados e a autenticação do Supabase, adotar o Storage é um caminho natural. Mas, se precisa apenas de armazenamento de objetos e tem muito tráfego com orçamento apertado, a taxa de saída zero do R2 é realmente atraente.
Você ainda tem alguma dúvida sobre o Supabase Storage ou já enfrentou algum problema com ele? Compartilhe sua experiência nos comentários.
Referências
- Storage CDN | Supabase Docs
- Storage Access Control | Supabase Docs
- Resumable Uploads | Supabase Docs
- Image Transformations | Supabase Docs
- Smart CDN | Supabase Docs
- Cloud Storage Pricing | BuildMVPFast
- Supabase Storage v3: Resumable Uploads
- GitHub Issue #563 — TUS Upload Stalling
Fluxo completo de upload de arquivos no Supabase Storage
Da criação do bucket à configuração de políticas RLS, implemente uploads seguros e controlados
⏱️ Estimated time: 30 min
- 1
Step 1: Criar um bucket no Storage
Crie um bucket no Supabase Dashboard:
• Acesse a página Storage e clique em 'Create a new bucket'
• Informe o nome do bucket, como avatars ou documents
• Escolha o modo Public ou Private
• Arquivos de buckets públicos podem ser acessados diretamente; buckets privados exigem uma URL assinada - 2
Step 2: Configurar políticas RLS
Defina o controle de acesso na tabela storage.objects:
```sql
-- Cada usuário só pode operar seus próprios arquivos
CREATE POLICY "Users manage own files"
ON storage.objects FOR ALL
USING (auth.uid()::text = (storage.foldername(name))[1]);
```
• bucket_id deve corresponder ao bucket de destino
• auth.uid() obtém o ID do usuário atual
• storage.foldername() interpreta o caminho do arquivo - 3
Step 3: Implementar o upload padrão
Use o método upload para enviar arquivos pequenos (<5 MB):
```typescript
const { error } = await supabase.storage
.from('bucket-name')
.upload('path/file.jpg', file, {
cacheControl: '3600',
upsert: false
});
```
• cacheControl define a duração do cache do navegador
• upsert: false impede que arquivos existentes sejam sobrescritos - 4
Step 4: Configurar upload em partes com TUS
Para arquivos grandes (>5 MB), use o protocolo TUS:
• Instale as dependências: npm install @uppy/tus tus-js-client
• Defina chunkSize: 6 * 1024 * 1024 (deve ser 6 MB)
• Configure o header Authorization para enviar o token JWT
• A URL de upload é válida por 24 horas - 5
Step 5: Otimizar o cache da CDN
Técnicas essenciais para melhorar a taxa de acerto do cache:
• Buckets públicos oferecem a melhor taxa de acerto
• Para arquivos atualizados com frequência, use um novo caminho em vez de sobrescrevê-los
• Use cacheNonce para forçar a atualização do cache
• Reutilize Signed URLs em cache, evitando gerá-las repetidamente
FAQ
Por que o chunkSize do Supabase Storage precisa ser de 6 MB?
Como escolher entre um bucket público e um bucket privado?
• Bucket público: recursos estáticos do site, imagens públicas e ilustrações de artigos — qualquer pessoa pode acessar
• Bucket privado: arquivos pessoais de usuários, conteúdo para assinantes e documentos confidenciais — o acesso exige uma Signed URL ou uma política RLS
Por que continuo vendo a versão antiga depois de atualizar um arquivo?
Qual é mais barato: Supabase Storage ou Cloudflare R2?
• Muitos downloads: o R2 economiza por não cobrar taxa de saída; no S3, ela custa US$ 0,09/GB
• Integração com Auth: o Supabase Storage é mais conveniente, pois reutiliza diretamente as políticas RLS
• Projeto que já usa Supabase: o Storage é a opção mais simples
• Armazenamento de objetos puro: o R2 tem custo menor
Como limitar uma política RLS para que cada usuário acesse somente os próprios arquivos?
```sql
CREATE POLICY "Users own files"
ON storage.objects FOR ALL
USING (
bucket_id = 'avatars'
AND auth.uid()::text = (storage.foldername(name))[1]
);
```
Assim, o usuário só pode operar arquivos cujo primeiro segmento do caminho corresponda ao próprio ID.
O que fazer quando a taxa de acerto do cache de uma Signed URL é baixa?
12 min de leitura · Publicado em: 14 abr 2026 · Atualizado em: 4 set 2026
Supabase na prática
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Supabase Realtime na prática: comparação entre três modos e desenvolvimento de aplicações colaborativas
O Supabase Realtime oferece três modos em tempo real: Postgres Changes, Presence e Broadcast. Este artigo compara as características de cada um e apresenta um exemplo completo de aplicação colaborativa, com código e configuração de segurança RLS.
Parte 5 de 7
Próximo
Supabase Edge Functions na prática: guia do runtime Deno e desenvolvimento com TypeScript
Aprenda a desenvolver com Supabase Edge Functions: entenda a arquitetura do runtime Deno e dos isolates V8, domine o fluxo de comandos da CLI e crie APIs RESTful com Hono, da depuração local à implantação em produção
Parte 7 de 7



Comentários
Entre com GitHub para comentar