O que vamos fazer
Componente de upload de avatar — usuário escolhe imagem do disco, vê preview imediato, e ao confirmar o arquivo vai pro bucket público avatares. URL fica disponível direto via CDN, e geramos um thumbnail 200×200 sem precisar pré-processar nada — só uma query string no URL.
O fluxo completo:
- Input
<input type="file" accept="image/*">com preview client-side. - Upload pra
avatares/{user_id}/avatar.jpgusandoupsert: true(substitui sempre o mesmo arquivo). - URL pública direta servida pelo CDN, latência ~30ms no Brasil.
- Thumbnail 200×200 via
?width=200&height=200&fit=cover. - Policy de Storage: cada user só consegue escrever no próprio diretório
{user_id}/.
Pré-requisitos
- Projeto SuperDB criado e SDK de auth instalado (@superdb/auth-js). Não existe
@superdb/superdb-js— para dados e storage, use HTTP direto ou@supabase/supabase-jscomapi.superdb.com.br/storage.superdb.com.br. - Usuário autenticado — a policy depende de
auth.uid(). - Bucket
avatarescriado público no Studio. - Policy de upload configurada (vamos criar abaixo).
Nota: bucket público não significa "qualquer um pode escrever". Significa que o URL de leitura é acessível sem token. Escrita continua controlada por policy.
Passo a passo
1. Criar o bucket no Studio
No Dashboard, vá em Storage → New bucket. Nome avatares, marque Public bucket, defina File size limit em 2 MB e Allowed MIME types em image/jpeg, image/png, image/webp. Salve.
2. Policy: só o dono pode subir
A convenção é organizar arquivos por {user_id}/ no início do path. Aí a policy compara auth.uid() com a primeira pasta:
-- Permitir INSERT só no próprio diretório
create policy avatar_own_insert
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'avatares'
and (storage.foldername(name))[1] = auth.uid()::text
);
-- Idem pra UPDATE (sobrescrever via upsert)
create policy avatar_own_update
on storage.objects
for update
to authenticated
using (
bucket_id = 'avatares'
and (storage.foldername(name))[1] = auth.uid()::text
);
-- DELETE opcional, se quiser permitir "remover avatar"
create policy avatar_own_delete
on storage.objects
for delete
to authenticated
using (
bucket_id = 'avatares'
and (storage.foldername(name))[1] = auth.uid()::text
);
3. Componente React com preview + upload
'use client'
import { useState } from 'react'
import { db } from '@/lib/superdb'
import { useUser } from '@/lib/auth'
export function AvatarUpload() {
const user = useUser()
const [preview, setPreview] = useState<string | null>(null)
const [url, setUrl] = 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
setPreview(URL.createObjectURL(file))
setBusy(true)
const ext = file.name.split('.').pop()
const path = `${user.id}/avatar.${ext}`
// Storage via HTTP direto — storage.superdb.com.br
const storageRes = await fetch(
`https://storage.superdb.com.br/object/avatares/${path}`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${ANON_KEY}`, 'apikey': ANON_KEY,
'Content-Type': file.type, 'x-upsert': 'true' },
body: file,
}
)
if (!storageRes.ok) { console.error(await storageRes.text()); setBusy(false); return }
const publicUrl = `https://storage.superdb.com.br/object/public/avatares/${path}`
setUrl(publicUrl)
// Dados via HTTP direto — api.superdb.com.br (sem /rest/v1/)
await fetch(`https://api.superdb.com.br/profiles?id=eq.${user.id}`, {
method: 'PATCH',
headers: { 'Authorization': `Bearer ${ANON_KEY}`, 'apikey': ANON_KEY,
'Content-Type': 'application/json', 'Prefer': 'return=minimal' },
body: JSON.stringify({ avatar_url: publicUrl }),
})
setBusy(false)
}
const thumb = url ? `${url}?width=200&height=200&fit=cover&quality=80` : null
return (
<div>
<input type="file" accept="image/*" onChange={handleFile} disabled={busy} />
{preview && <img src={thumb ?? preview} alt="Avatar" width={200} height={200} />}
</div>
)
}
Dica: use upsert: true com nome fixo (avatar.jpg). Cada user tem 1 arquivo no diretório dele e não acumula lixo. Mudar o avatar = sobrescrever o mesmo path.
4. Thumbnail on-demand
Storage do SuperDB faz resize/crop direto pela URL. Sem job, sem fila, sem pré-renderização. Os parâmetros suportados:
https://<projeto>.superdb.com.br/storage/v1/object/public/avatares/abc/avatar.jpg
?width=200 # largura final em px
&height=200 # altura final em px
&fit=cover # cover | contain | fill
&quality=80 # 1–100 (JPEG/WebP)
&format=webp # converte pra WebP — economiza 30%
A primeira request com determinados parâmetros é processada (~200ms). As próximas vêm do cache do CDN.
Resultado
Pronto. Você tem:
- Avatar servido por CDN com latência <50ms no Brasil.
- Cada user só consegue editar o próprio avatar (RLS no
storage.objects). - Thumbnail gerado via query string — sem código de processamento.
- URL salva no
profiles.avatar_urlpra consultar junto com o resto do perfil.
Variações
Crop client-side antes do upload
Pra forçar avatar quadrado independente da imagem original, use react-image-crop. Aplica o crop num <canvas>, exporta blob, faz upload do blob.
Múltiplos tamanhos pré-renderizados
Se você sabe os tamanhos que vai usar (32, 64, 200, 400), gere as URLs uma vez no signup e salve cada uma. Evita o cold-start dos 200ms do primeiro request.
Fallback Gravatar
Quando avatar_url for null, mostre o Gravatar baseado no email (md5(email.trim().toLowerCase())).
Erros comuns
Bucket privado por engano: se o bucket não estiver marcado público, getPublicUrl() retorna uma URL que sempre dá 400. Vá em Storage → bucket → Configuration → Public.
Policy sem WITH CHECK: só USING não restringe o INSERT — qualquer authenticated user pode subir avatar pra qualquer pasta. O WITH CHECK é o que valida na inserção.
Sem maxFileSize: sem limite no bucket, alguém sobe imagem de 50 MB. Configure 2 MB no Studio. O SDK retorna erro 413 quando excede.
Cache antigo após troca de avatar: o CDN cacheia agressivamente. Pra forçar refresh, adicione cache buster: ${url}?v=${Date.now()}.