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

Eu estava olhando a mensagem de erro no painel. Meia hora depois de colocar o upload de avatares em produção, chegou o relato de um usuário: o avatar de todo mundo havia virado o da mesma pessoa.
Ao investigar, encontrei o problema na configuração da política RLS do Storage — ela simplesmente não existia. O bucket era público, o caminho de upload não isolava os usuários e qualquer upload podia sobrescrever o arquivo de outra pessoa. Um descuido nas permissões quase se transformou em um incidente de produção.
O Supabase Storage é fácil de começar a usar, mas exige alguns cuidados para funcionar bem de verdade — controle de acesso, aceleração por CDN e transformação de imagens têm suas próprias armadilhas. Aqui reuni os problemas que enfrentei e o que aprendi ao resolvê-los.
1. Primeiros passos: upload padrão de arquivos
Vamos começar pelo básico: enviar um arquivo.
Criar um bucket
Abra o painel do Supabase, encontre Storage no menu lateral e clique em “New bucket”. Dê um nome ao bucket, como avatars para avatares ou posts para imagens dos artigos. Uma opção perguntará “Make this bucket public?” — não marque ainda; a seção sobre permissões explica essa escolha em detalhes.
Minha prática é guardar arquivos sensíveis em buckets privados e recursos estáticos em buckets públicos. Por padrão, começo com um bucket privado e ajusto depois, quando necessário.
Código de upload com o SDK
Supondo que @supabase/supabase-js já esteja instalado, o código é simples:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
)
// Fazer upload do arquivo
async function uploadFile(file: File) {
const filePath = `uploads/${Date.now()}-${file.name}`
const { data, error } = await supabase.storage
.from('avatars') // Nome do bucket
.upload(filePath, file, {
cacheControl: '3600', // Cache por 1 hora
upsert: false // Retorna erro se o arquivo já existir; não sobrescreve
})
if (error) {
console.error('Falha no upload:', error.message)
return null
}
return data.path // Retorna o caminho do arquivo
}
Para ser sincero, já escrevi esse trecho de código mais de dez vezes. O ponto essencial é o desenho de filePath: adiante explico por que usar um prefixo com timestamp e como isolar os usuários.
Limites de tamanho dos arquivos
Segundo a documentação oficial, o upload padrão aceita arquivos de até 5 GB. Na prática, ele oferece a melhor experiência para arquivos menores que 6 MB. Acima de 6 MB, a recomendação é usar o protocolo TUS para permitir a retomada do upload.
O que é TUS? Em termos simples, ele permite retomar o envio de arquivos grandes. Se a conexão cair, o upload continua do ponto em que parou quando a rede voltar, em vez de recomeçar. Para vídeos e imagens grandes, a diferença é enorme: imagine a conexão cair quando o usuário já enviou 90% do arquivo. Sem TUS, seria preciso começar novamente.
O upload com TUS exige configuração adicional. Se você ainda não precisa dele, o upload padrão atende à maioria dos cenários.
// Exemplo de upload com TUS (recomendado para arquivos grandes)
const { data, error } = await supabase.storage
.from('videos')
.upload('large-video.mp4', file, {
duplex: 'half', // Ativa o upload por streaming
// O TUS cuida automaticamente da retomada do upload
})
2. Configuração de segurança: políticas RLS em detalhes
Agora voltamos ao problema que enfrentei às três da manhã: as permissões não estavam configuradas e os arquivos podiam ser sobrescritos livremente.
O Storage do Supabase, assim como o banco de dados, usa PostgreSQL por baixo. Por isso, o controle de acesso também segue o modelo RLS (Row Level Security). O bucket funciona como uma tabela, e cada arquivo corresponde a um registro.
Buckets públicos e privados
Ao criar um bucket, você escolhe entre “Public bucket” e “Private bucket”.
Public bucket: qualquer pessoa pode ler os arquivos, sem autenticação. É adequado para recursos estáticos públicos, como avatares e logotipos do site.
Private bucket: exige autenticação para acesso. Mas existe uma armadilha: a autenticação é apenas a porta de entrada. Quem pode ler ou gravar depende das políticas RLS.
Minha recomendação é criar um bucket privado por padrão, a menos que os arquivos sejam realmente públicos. É muito mais seguro configurar as permissões primeiro e abrir o acesso depois do que tornar tudo público e tentar corrigir em seguida.
Tipos de política RLS
Na página Policies do Storage, você verá quatro operações:
- SELECT: ler arquivos (download ou obtenção da URL)
- INSERT: enviar novos arquivos
- UPDATE: atualizar ou sobrescrever arquivos existentes
- DELETE: excluir arquivos
Cada operação pode ter sua própria política. A configuração mais comum é esta:
-- O usuário só pode operar nos próprios arquivos
CREATE POLICY "Users manage own files"
ON storage.objects FOR ALL
USING (auth.uid()::text = (storage.foldername(name))[1]);
Esse SQL pode parecer um pouco complexo, então vamos dividi-lo:
auth.uid()obtém o ID do usuário autenticadostorage.foldername(name)extrai o nome do primeiro diretório do caminho do arquivo- Se o caminho for
user123/avatar.jpg, por exemplo, o primeiro diretório seráuser123
Portanto, a lógica completa da política é esta: o usuário só pode operar em um arquivo quando o primeiro diretório do caminho corresponde ao seu ID. Essa é a ideia central do isolamento por usuário.
Como implementar o isolamento por usuário
Como fazer isso na prática? Coloque o ID do usuário no primeiro nível do caminho durante o upload:
async function uploadAvatar(userId: string, file: File) {
// Estrutura do caminho: ID do usuário/nome do arquivo
const filePath = `${userId}/avatar-${Date.now()}.jpg`
const { data, error } = await supabase.storage
.from('avatars')
.upload(filePath, file)
return data?.path
}
Assim, os arquivos de cada usuário ficam em sua própria “pasta”. A política RLS permite apenas operações em caminhos iniciados pelo ID do usuário, mantendo os arquivos dos demais fora de alcance.
Gerar uma URL de acesso assinada
O acesso direto a um arquivo de bucket privado retorna 404. É preciso gerar uma URL assinada:
// Gerar um link temporário de acesso (válido por 1 hora)
const { data, error } = await supabase.storage
.from('avatars')
.createSignedUrl('user123/avatar.jpg', 3600)
console.log(data?.signedUrl) // URL completa com assinatura
Você escolhe a validade da assinatura. Um prazo muito longo não é seguro; um prazo curto demais prejudica a experiência. Em geral, de 1 a 4 horas é uma faixa adequada.
Se quiser tornar o arquivo completamente público sem alterar a configuração do bucket, use getPublicUrl:
const { data } = supabase.storage
.from('public-assets')
.getPublicUrl('logo.png')
// Esta URL não precisa de assinatura; qualquer pessoa pode acessá-la
Erros comuns na configuração das políticas
Estes são alguns dos problemas que já enfrentei:
-
Esquecer a política INSERT: o usuário consegue entrar, mas não consegue enviar arquivos. A mensagem de erro é “new row violates row-level security policy”.
-
Criar uma política permissiva demais: usar
USING (true), por exemplo, permite que todos operem em todos os arquivos. Na prática, equivale a não configurar RLS. -
Usar uma estrutura de caminho inadequada: se o ID do usuário não estiver no primeiro nível, a extração feita por
foldernamenão funcionará como esperado. Já cometi esse erro ao usaruploads/user123/file.jpg; o valor extraído erauploads, e a verificação da política falhava.
Ao configurar uma política, teste-a primeiro no editor SQL do painel. Confirme que a lógica está correta antes de aplicá-la ao ambiente de produção.
3. Mais desempenho: Smart CDN e transformação de imagens
Os arquivos já podem ser enviados e as permissões estão configuradas. A próxima questão é: como fazê-los carregar mais rápido?
Como funciona o Smart CDN
O Smart CDN do Supabase não é um CDN comum. Ele ajusta automaticamente a estratégia de cache com base na frequência de acesso: arquivos populares ficam armazenados por mais tempo, enquanto arquivos pouco acessados permanecem por períodos menores.
Segundo a documentação oficial, a invalidação do cache leva no máximo 60 segundos para ser sincronizada globalmente. Isso significa que, depois de atualizar um arquivo em Tóquio, os usuários de Nova York verão a versão mais recente em até 60 segundos. É muito mais rápido que os minutos ou até horas de um CDN tradicional.
No entanto, o Smart CDN é um recurso pago e exige o Pro Plan, de US$ 25 por mês. No Free Plan, os arquivos continuam acessíveis, mas sem a aceleração do CDN: eles são servidos diretamente pelos servidores do Supabase.
Parâmetros de transformação de imagens
Gosto bastante desse recurso: você não precisa implementar o redimensionamento nem o recorte, porque o Supabase faz tudo por meio de parâmetros na URL.
Parâmetros básicos:
?width=300&height=200 // Define as dimensões
?resize=contain // Mantém a proporção, sem recortar
?resize=cover // Preenche as dimensões e recorta o excedente
?quality=80 // Qualidade da imagem (1-100)
?format=webp // Converte para WebP e reduz o tamanho
Você pode combiná-los:
const baseUrl = supabase.storage
.from('avatars')
.getPublicUrl('user123/avatar.jpg').data.publicUrl
// Gerar uma miniatura
const thumbnailUrl = `${baseUrl}?width=100&height=100&resize=cover`
Limites da transformação de imagens:
- Faixa de dimensões: de 1 a 2500 pixels
- Tamanho do arquivo original: no máximo 25 MB
- Formatos compatíveis: JPEG, PNG, WebP, GIF e AVIF
Quando o limite é ultrapassado, o serviço retorna um erro. Certa vez, tentei redimensionar uma imagem original de 30 MB e ela foi rejeitada imediatamente.
Preço e franquia gratuita por projeto
A transformação de imagens é cobrada pela quantidade de transformações, não pelo espaço de armazenamento.
As primeiras 100 imagens transformadas por projeto a cada mês são gratuitas. Depois disso, o preço é de US$ 5 por 1.000 imagens.
Para um projeto pessoal ou uma equipe pequena, 100 imagens costumam ser suficientes. No meu próprio blog, transformo apenas algumas dezenas de avatares e imagens de artigos por mês. A menos que você esteja criando uma rede social de imagens parecida com o Instagram, esse custo provavelmente não será uma preocupação.
Integração com Next.js: Image Loader
Se você usa Next.js, pode configurar o Image Loader do Supabase para que o next/image aplique automaticamente as transformações:
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './supabase-image-loader.js',
}
}
Em seguida, crie o arquivo do loader:
// supabase-image-loader.js
export default function supabaseLoader({ src, width, quality }) {
const params = new URLSearchParams()
params.set('width', width.toString())
params.set('quality', (quality || 75).toString())
params.set('format', 'webp')
return `${src}?${params.toString()}`
}
Assim, quando você usar <Image src="..." width={300} /> no Next.js, os parâmetros de transformação serão acrescentados automaticamente.
A exigência do Pro Plan
O Smart CDN e a transformação de imagens, mencionados acima, exigem o Pro Plan. Quem usa o Free Plan tem acesso apenas aos recursos básicos de upload e download.
Vale a pena fazer o upgrade? Depende das necessidades do projeto. Se você armazena apenas alguns avatares, o Free Plan é suficiente. Mas, se precisa processar muitas imagens e otimizar o desempenho, a aceleração por CDN e a transformação de imagens do Pro Plan economizam bastante trabalho: você não precisa configurar seu próprio CDN nem criar um serviço de processamento de imagens.
Minha escolha é começar os testes com o Free Plan antes de colocar o projeto em produção e migrar para o Pro quando o tráfego se estabilizar. Afinal, US$ 25 por mês não é um valor desprezível.
4. Caso prático: configuração completa de um blog
Depois de tanta teoria, vale olhar um caso completo. Esta é a configuração do Storage no meu projeto de blog, do zero até o funcionamento.
Cenário: avatares de usuários e imagens de artigos
São necessários dois buckets:
avatars: avatares dos usuários; bucket privado em que cada usuário só pode operar no próprio avatarpost-images: imagens dos artigos; bucket privado em que os autores podem fazer upload e qualquer pessoa pode ler (com URL assinada)
Etapa 1: criar os buckets
No painel:
- Acesse Storage > New bucket, use o nome
avatarse marque Private - Crie
post-imagesda mesma forma
Etapa 2: configurar as políticas RLS
Políticas do bucket avatars:
-- Permitir que qualquer pessoa leia os avatares (leitura pública)
CREATE POLICY "Anyone can view avatars"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');
-- O usuário só pode enviar e atualizar o próprio avatar
CREATE POLICY "Users manage own avatar"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);
-- O usuário só pode excluir o próprio avatar
CREATE POLICY "Users delete own avatar"
ON storage.objects FOR DELETE
USING (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);
Políticas do bucket post-images:
-- Autores podem enviar imagens de artigos (supondo que tenham a função author)
CREATE POLICY "Authors can upload post images"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'post-images'
AND auth.jwt() ->> 'role' = 'author'
);
-- Qualquer pessoa pode ler as imagens dos artigos
CREATE POLICY "Public read post images"
ON storage.objects FOR SELECT
USING (bucket_id = 'post-images');
Etapa 3: código de upload no frontend
Componente de upload do avatar:
async function handleAvatarUpload(file: File) {
const user = await supabase.auth.getUser()
if (!user.data.user) return alert('Faça login primeiro')
// Caminho: ID do usuário/avatar.jpg (nome fixo; cada upload substitui o avatar anterior)
const filePath = `${user.data.user.id}/avatar.jpg`
const { error } = await supabase.storage
.from('avatars')
.upload(filePath, file, { upsert: true })
if (!error) {
// Obter a URL pública (pois a política SELECT permite a leitura por qualquer pessoa)
const url = supabase.storage.from('avatars').getPublicUrl(filePath)
setUserAvatar(url.data.publicUrl)
}
}
Upload de imagens dos artigos:
async function handlePostImageUpload(file: File) {
const filePath = `posts/${Date.now()}-${file.name}`
const { data, error } = await supabase.storage
.from('post-images')
.upload(filePath, file)
if (!error) {
// Gerar uma URL assinada, válida por 24 horas
const { data: urlData } = await supabase.storage
.from('post-images')
.createSignedUrl(filePath, 86400)
insertImageToEditor(urlData?.signedUrl)
}
}
Etapa 4: testar e validar
Antes de entrar em produção, verifique estes pontos essenciais:
- Um usuário não autenticado consegue ver as imagens dos artigos? (Deveria conseguir, pois a política SELECT permite.)
- Um usuário comum consegue enviar imagens de artigos? (Não deveria; apenas a função author tem permissão.)
- O usuário A consegue sobrescrever o avatar do usuário B? (Não deveria, graças ao isolamento pelo caminho.)
Teste cada ponto e confirme que as políticas estão corretas. Não quero repetir a lição que aprendi às três da manhã.
Conclusão
O Supabase Storage tem três pilares: upload, permissões e aceleração.
O upload é a parte mais simples e leva poucas linhas de código. Já a configuração de permissões merece atenção: uma política RLS não deve ser configurada uma vez e esquecida; ela precisa ser testada repetidamente de acordo com os cenários do produto. O CDN e a transformação de imagens são recursos adicionais disponíveis no Pro Plan, mas podem economizar bastante tempo de desenvolvimento.
Minha experiência é começar pelo upload básico e pelas permissões, garantindo primeiro que não exista risco de segurança. Adicione o CDN quando houver necessidade de desempenho e a transformação quando o produto exigir processamento de imagens. Avance passo a passo, sem tentar resolver tudo de uma vez.
Se você também usa o Supabase Storage, compartilhe os problemas que já encontrou. Provavelmente não fui a única pessoa a aprender essa lição às três da manhã.
Configuração completa do Supabase Storage
Processo prático completo, desde a criação do bucket e a configuração das permissões até a aceleração por CDN
⏱️ Estimated time: 30 min
- 1
Step 1: Criar o bucket
Crie um bucket privado no painel do Supabase:
• Acesse Storage > New bucket
• Informe um nome (como avatars)
• Marque Private (a opção privada é recomendada por padrão)
• Clique em Create bucket - 2
Step 2: Configurar a política RLS
Configure o Row Level Security do bucket:
• Acesse Storage > selecione o bucket > Policies
• Clique em New Policy
• Selecione o tipo de operação (SELECT/INSERT/UPDATE/DELETE)
• Escreva a regra da política (como o isolamento por usuário)
• Teste antes de aplicá-la ao ambiente de produção - 3
Step 3: Fazer upload de arquivos
Use o SDK para enviar arquivos:
• Defina a estrutura do caminho (como userId/filename)
• Chame storage.from().upload()
• Configure os parâmetros cacheControl e upsert
• Trate os erros de upload e o caminho retornado - 4
Step 4: Configurar CDN e transformação de imagens (opcional)
Após migrar para o Pro Plan, você poderá usar os recursos avançados:
• O Smart CDN armazena automaticamente em cache os arquivos mais acessados
• Parâmetros de transformação de imagens na URL (width/height/format)
• Integração com o Image Loader do Next.js
• Monitoramento da franquia gratuita (100 imagens/mês)
FAQ
Qual é a diferença entre um bucket público e um bucket privado?
Como configurar uma política RLS para isolar os usuários?
• Defina o caminho de upload como userId/filename
• Na política, use auth.uid()::text = (storage.foldername(name))[1]
• Assim, o usuário só poderá operar em caminhos que começam com o próprio ID
Como compartilhar externamente um arquivo de um bucket privado?
Quais são os limites para upload de arquivos?
Quais parâmetros e limites são aceitos na transformação de imagens?
• width/height: dimensões (faixa de 1 a 2500 pixels)
• resize: contain (mantém a proporção) ou cover (recorta para preencher)
• quality: qualidade (1-100)
• format: webp/jpeg/png/gif/avif
Limite: o arquivo original não pode ultrapassar 25 MB
O Smart CDN e a transformação de imagens são recursos pagos?
12 min de leitura · Publicado em: 9 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 Auth na prática: verificação de e-mail, OAuth e gerenciamento de sessões
Configure o Supabase Auth com verificação de e-mail, OAuth, sessões JWT e fluxo PKCE para criar uma autenticação segura em aplicações web e SSR.
Parte 3 de 7
Próximo
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



Comentários
Entre com GitHub para comentar