Pular para o conteúdo
📖 COOKBOOK

Subir avatar em 12 linhas.

Upload de avatar com CDN integrado. URL pública direta + thumbnail gerado on-demand via query string. Mostra como configurar políticas pra cada user só editar o próprio avatar.

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.jpg usando upsert: 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}/.
app.superdb.com.br/projeto/storage/avatares
v0.4.2
avatares/ ×
1// 8c4f.../avatar.jpg — 84 KB
2// 9b21.../avatar.png — 142 KB
3// e07a.../avatar.webp — 56 KB

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-js com api.superdb.com.br / storage.superdb.com.br.
  • Usuário autenticado — a policy depende de auth.uid().
  • Bucket avatares criado 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:

schema.sql
-- 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

src/components/AvatarUpload.tsx
'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:

URL de transformação
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_url pra 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: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()}.

Essa página ajudou?