Pular para o conteúdo
Erick Schaedler

Em produção

Photo Album

Um PWA de álbum de fotos colaborativo construído inteiro dentro do plano gratuito da Cloudflare — um Worker, D1 e R2, sem nenhum serviço pago.

O problema

Duas pessoas querem uma linha do tempo de fotos privada e compartilhada sem pagar por armazenamento em nuvem nem manter um servidor — compartilhar fotos com outra pessoa não devia exigir mensalidade.

O que construí

Um único Worker da Cloudflare servindo a API e o frontend em SvelteKit, D1 para os dados, um bucket privado no R2 para as fotos, e um service worker para uso offline e instalável.

Tela de entrada do Photo Album — o app é privado, então tudo depois daqui exige conta.
Tela de entrada do Photo Album no celular.

Decisões técnicas

O aplicativo inteiro roda atrás de um único Worker da Cloudflare — não um deploy separado no Pages e outro para a API. Requisições para /api/* vão para o app em Hono; todo o resto cai para o build estático do SvelteKit servido como asset do próprio Worker. Escolhi isso de propósito em vez de separar frontend e backend em dois deploys: dois deploys e uma configuração de CORS não iam comprar nada aqui que um Worker com uma regra de rota já não resolve.

Os dados ficam no D1, com o Drizzle como camada de schema e consulta — as migrações são arquivos SQL simples gerados a partir de um schema em TypeScript, não escritos à mão toda vez. Para um app guardando metadados de fotos de duas pessoas, o teto do plano gratuito do D1 não é uma restrição de verdade; é o limite de armazenamento do R2 que importa, porque é lá que os bytes das fotos ficam.

O bucket do R2 não tem domínio público nenhum — não existe uma URL pública adivinhável para nenhuma foto. Uma foto só é servida através de uma rota autenticada: o Worker confere a sessão da requisição, confirma que a foto pertence ao espaço de quem está pedindo, e só então transmite o objeto direto do R2, com uma content-security-policy estrita e cabeçalho nosniff na resposta. Isso é uma escolha deliberada em vez de gerar uma URL assinada — a checagem de acesso acontece a cada requisição, não uma vez só na hora de gerar o link.

Ficar no plano gratuito significou realmente conferir os limites dele, não só evitar qualquer coisa com preço. O hash de senha com 100 mil iterações de PBKDF2 arriscava estourar o teto de tempo de CPU por requisição do plano gratuito no login, então medi isso em produção em vez de chutar — voltou em menos de meio segundo, tranquilamente dentro do limite — e deixei um plano B documentado (menos iterações, um formato de hash versionado) pronto para o caso de um padrão de login futuro estourar isso.

Não existe cadastro aberto. A primeira conta é criada por uma rota de configuração de uso único, e a única forma de mais alguém entrar é um link de convite de uso único que expira em sete dias — o link em si nunca é guardado, só o hash dele. Toda consulta na API é restrita ao espaço de quem está pedindo, e uma foto que pertence ao espaço de outra pessoa devolve exatamente o mesmo 404 de uma foto que não existe, então não dá para diferenciar os dois casos de fora.

Resultado

No ar e instalável como PWA, rodando de ponta a ponta no plano gratuito da Cloudflare, sem conta de hospedagem separada.

Stack

  • Cloudflare Workers
  • D1
  • R2
  • Hono
  • SvelteKit
  • TypeScript
  • Drizzle ORM