O que vamos fazer
Componente de avatar: o usuário escolhe uma imagem, vê a prévia na hora e, ao confirmar, o arquivo vai para o bucket público proj_loja_avatares, na pasta dele. A URL pública abre em qualquer <img>, e a miniatura 200×200 sai da mesma imagem, pela URL.
O fluxo completo:
<input type="file" accept="image/*">com prévia no navegador.- Upload para
{user_id}/avatarcomupsert: true: trocar de foto sobrescreve o mesmo arquivo. - URL pública de
getPublicUrl(), guardada emprofiles.avatar_url. - Miniatura: troque
/object/por/render/image/na URL e acrescente?width=200&height=200&resize=cover.
Pré-requisitos
- Projeto SuperDB criado e SDK instalado (@superdb/client) — com os nomes do supabase-js, já traz
.auth,.from()e.storage. - Usuário logado: o SDK troca a sessão dele por um token de storage, sozinho.
- Tabela
profilescom a colunaavatar_url, e uma policy que deixe o usuário atualizar a própria linha.
Quem lê e quem grava: num bucket público, qualquer pessoa com a URL lê. Gravar exige um usuário logado do projeto ou a chave de servidor — e hoje qualquer usuário logado grava em qualquer pasta do projeto, porque o Storage ainda não separa arquivos por usuário. A pasta {user_id}/ organiza, mas não protege.
Passo a passo
1. Criar o bucket no painel
No painel, vá em Storage → Novo bucket. Nome avatares, visibilidade Público e limite de 2 MB. O painel cria o bucket como proj_loja_avatares (proj_ + o slug do projeto + o nome) — é esse nome, o que aparece na lista, que vai no código.
O Storage não filtra o tipo de arquivo: confira file.type no app antes de subir.
2. Componente React com prévia e upload
'use client'
import { useState } from 'react'
import { db } from '@/lib/superdb'
import { useUser } from '@/lib/auth'
const BUCKET = 'proj_loja_avatares'
const TIPOS = ['image/jpeg', 'image/png', 'image/webp']
export function AvatarUpload() {
const user = useUser()
const [preview, setPreview] = useState<string | null>(null)
const [thumb, setThumb] = useState<string | null>(null)
const [busy, setBusy] = useState(false)
async function handleFile(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file || !user) return
if (!TIPOS.includes(file.type)) return alert('Envie JPEG, PNG ou WebP')
if (file.size > 2 * 1024 * 1024) return alert('Imagem acima de 2 MB')
setPreview(URL.createObjectURL(file))
setBusy(true)
const path = `${user.id}/avatar`
// O SDK troca a sessão por um token de storage sozinho
const { error } = await db.storage.from(BUCKET)
.upload(path, file, { upsert: true, contentType: file.type })
if (error) { console.error(error); setBusy(false); return }
const { data: { publicUrl } } = db.storage.from(BUCKET).getPublicUrl(path)
await db.from('profiles').update({ avatar_url: publicUrl }).eq('id', user.id)
// Miniatura: a mesma imagem, pelo caminho de transformação.
// O v= só faz o navegador buscar de novo depois da troca.
setThumb(publicUrl.replace('/object/', '/render/image/')
+ `?width=200&height=200&resize=cover&quality=80&v=${Date.now()}`)
setBusy(false)
}
return (
<div>
<input type="file" accept="image/*" onChange={handleFile} disabled={busy} />
{preview && <img src={thumb ?? preview} alt="Avatar" width={200} height={200} />}
</div>
)
}
Dica: com upsert: true e nome fixo (avatar), cada usuário tem um arquivo só e o bucket não acumula lixo. Trocar a foto é sobrescrever o mesmo caminho.
3. Miniatura pela URL
O Storage redimensiona e recorta pela URL, sem job e sem fila. É outro caminho, não uma query na URL do arquivo:
https://storage.superdb.com.br/render/image/public/proj_loja_avatares/<user_id>/avatar
?width=200 # largura em px
&height=200 # altura em px
&resize=cover # cover | contain | fill
&quality=80 # de 20 a 100
Sai em WebP quando o navegador aceita e, se não, no formato original. Só funciona em bucket público.
Resultado
Pronto. Você tem:
- Avatar numa URL pública, que qualquer
<img>abre. - Miniatura gerada pelo Storage, sem código de processamento no app.
- A URL salva em
profiles.avatar_url, para vir junto com o resto do perfil.
Variações
Recorte no navegador antes do upload
Para forçar avatar quadrado independente da imagem original, use react-image-crop. Ele aplica o recorte num <canvas>, exporta um blob, e você sobe o blob.
Vários tamanhos
Cada tamanho é só outra URL: mude width e height (32, 64, 200, 400). Não precisa guardar cópias.
Fallback Gravatar
Quando avatar_url for null, mostre o Gravatar baseado no email (md5(email.trim().toLowerCase())).
Erros comuns
Nome curto do bucket: com 'avatares' no lugar de 'proj_loja_avatares', o Storage não acha nada — a URL pública dá 400. Use o nome como aparece no painel.
?width= na URL do arquivo: /object/public/…?width=200 devolve a imagem original inteira. A transformação só existe em /render/image/public/….
Bucket privado por engano: a URL de getPublicUrl() só abre em bucket público. No painel, a visibilidade do bucket precisa ser Público.
Foto antiga na tela depois da troca: as respostas do Storage mandam o navegador revalidar, mas uma imagem já carregada na mesma página pode ficar. Acrescente &v=<timestamp> depois de cada upload, como no componente.