Toda plataforma de DOOH resolve o mesmo problema básico: uma tela em algum lugar precisa saber o que tocar, agora, mesmo que a internet daquele ponto caia três vezes por dia. A partir daí as decisões divergem bastante.
A nossa foi rodar tudo na borda da Cloudflare. Sem servidor de aplicação, sem fila gerenciada de terceiros, sem Docker em lugar nenhum — nem em desenvolvimento.
O desenho
Quatro peças fazem o trabalho pesado:
- Workers servem a API inteira (Hono). Nenhuma requisição de player passa por um datacenter fixo: ela é atendida no ponto de presença mais perto da tela.
- D1 guarda o catálogo global — tenants, usuários, diretório de players.
- R2 guarda a mídia. Sem custo de egresso, o que importa quando cada tela baixa os mesmos arquivos de vídeo.
- Durable Objects guardam o estado de cada cliente. Um
TenantDOpor organização, com SQLite embutido.
A quarta é a que mais define o resto.
Um Durable Object por cliente
O instinto normal seria uma tabela playlists com uma coluna tenant_id, e um WHERE em toda query. Funciona, e é onde quase todo SaaS começa.
Nós fomos por outro caminho: cada organização tem o seu próprio Durable Object, com o seu próprio banco. Playlists, agendamentos, provas de veiculação e assets daquele cliente vivem lá dentro, e em lugar nenhum além disso.
Isso compra três coisas de uma vez:
- Isolamento por construção. Não existe query que possa vazar dados entre clientes, porque não existe uma tabela compartilhada para vazar. O erro clássico de esquecer o
WHERE tenant_iddeixa de ser possível. - Coordenação sem fila. Um DO é single-threaded por definição. Quando dez telas do mesmo cliente reconectam ao mesmo tempo depois de uma queda de energia, elas conversam com o mesmo objeto, em ordem. Não precisamos de lock distribuído para nada disso.
- Localidade. O objeto migra para perto de quem mais o usa.
O preço é real e vale dizer: você perde a query global. "Quantas telas estão ativas na plataforma inteira?" deixa de ser um SELECT COUNT(*) e vira um fan-out ou uma projeção que você mantém de propósito. Toda pergunta que atravessa clientes passa a exigir trabalho explícito.
Para o nosso caso, valeu. As perguntas que importam no dia a dia — "essa playlist mudou?", "essa tela está viva?", "o que tocou ontem nessa tela?" — são todas dentro de um cliente só.
O que doeu
Migrações dentro do DO. Migrar um banco central é uma operação; migrar N bancos, cada um acordando quando o cliente aparece, é outra. Nossa regra hoje é dura: o SQL que roda no boot do TenantDO é só DDL e tem que ser barato. Qualquer backfill pesado vai para um alarm ou waitUntil, fora do caminho crítico. Aprendemos isso da forma normal — um boot que ficou lento demais.
Limites de CPU por requisição. Workers não são o lugar para transcodificar vídeo. Nada que seja pesado de CPU roda em linha; ou vai para um serviço próprio, ou vira Workflow.
Observabilidade. Estado espalhado por milhares de objetos não aparece num dashboard sozinho. Custa esforço deliberado projetar o que importa para fora.
O que não precisamos construir
A parte mais fácil de subestimar é o que some do backlog.
Não temos servidor para escalar, nem cluster para atualizar, nem certificado TLS para renovar, nem CDN para configurar na frente da mídia. wrangler dev sobe o stack inteiro na máquina do dev — sem docker compose, sem Postgres local, sem seis containers para lembrar de derrubar.
Para um time pequeno operando uma rede de telas que precisa ficar de pé, essa é a decisão que mais rende.

