Skip to main content
Duas coisas podem ser enviadas: o avatar de um usuário e o logo de uma organização. As duas funcionam igual, e nenhuma manda bytes pela API.
Exige storage compatível com S3 (S3, R2, MinIO). Sem ele as duas rotas respondem 501 uploads_unavailable e GET /v1/session informa uploads_available: false.

O fluxo

1

Peça uma URL assinada

2

Faça o PUT direto no bucket

Envie exatamente os headers que voltaram — eles fazem parte da assinatura.
3

Salve a URL pública

Salvar é uma chamada separada de propósito. O upload pode falhar no meio, e um perfil apontando para um objeto escrito pela metade seria pior que um ainda apontando para a imagem antiga.

O logo da organização

Idêntico, com duas diferenças: exige organization:update, e a URL é salva com PATCH /v1/organization.

O que é permitido

Uma allowlist, não uma blocklist — deliberadamente. Esses arquivos são servidos de volta a partir de um host público, e text/html entre eles seria um vetor de XSS armazenado naquele host. O tipo e o tamanho fazem parte da assinatura, então é o bucket que os impõe. A verificação no lado da API existe para te dar um erro legível, não para ser a imposição.

Substituindo uma imagem

Cada upload ganha uma chave de objeto aleatória, então substituir uma imagem nunca sobrescreve o objeto antigo. Uma URL em cache num CDN ou num e-mail não pode começar a servir a foto de outra pessoa. O objeto antigo fica para trás. Limpar é um job de manutenção, não parte da requisição.

Fazendo isso do navegador

O painel faz exatamente os três passos acima a partir do código no cliente. A URL pré-assinada é de vida curta e delimitada a um objeto, um content type e um tamanho, então entregá-la ao navegador não dá nada reutilizável.
No painel isso passa pelo proxy /api/* do app Next, que anexa o token de sessão a partir do cookie httpOnly. O PUT vai direto para o bucket e não carrega credencial nenhuma nossa.