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.


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