Uma plataforma de corridas inteira na borda: a arquitetura do Carioca Taxi
Por que reescrevemos o app, e como uma API, um banco, o tempo real e as tarefas agendadas cabem na rede da Cloudflare sem nenhum servidor para administrar.
Este é o 1º artigo da série “Construindo o Carioca Taxi”.
Um app de táxi parece simples visto de fora: o passageiro pede, o motorista aceita, o carro chega. Por dentro, é um sistema que precisa achar o motorista certo em segundos, mostrar o carro andando no mapa, cobrar e repassar dinheiro sem errar um centavo, e continuar funcionando quando o celular do motorista entra num túnel, fica sem bateria ou perde a rede no meio da Avenida Brasil.
Este é o primeiro artigo de uma série sobre como construímos o Carioca Taxi. Aqui contamos a arquitetura inteira: as peças, por que escolhemos cada uma e o que custou cada escolha.
Por que reescrever
A primeira versão do Carioca Taxi era um app web empacotado como aplicativo. Ela serviu para provar que táxi do Rio pedido pelo celular fazia sentido, mas tinha um limite que nenhum ajuste resolvia: um app web não controla o celular. O sistema operacional pausa a página quando a tela apaga, o GPS em segundo plano é instável, as notificações dependem do navegador e cada falha de rede vira uma tela em branco.
Para o motorista, isso significava perder corridas com o celular no painel do carro. Decidimos reescrever com dois princípios:
- Robustez, não redesenho. As telas, as regras e o jeito de trabalhar que os taxistas já conheciam ficaram iguais. Comparamos cada tela nova com a antiga, lado a lado, antes de seguir.
- Nenhum servidor para administrar. Somos uma equipe pequena. Cada hora gasta atualizando sistema operacional, configurando balanceador ou acordando de madrugada para reiniciar um processo é uma hora a menos no produto.
As peças
Apps em React Native
O app do passageiro e o do motorista são React Native com Expo. Ganhamos acesso de verdade ao celular: serviço em primeiro plano para o GPS, canais de notificação com som próprio, mapas nativos e sensores. E mantivemos uma única base de código em TypeScript.
O Expo também nos dá atualização pelo ar. Uma correção de texto ou de regra de tela chega aos celulares em minutos, sem passar pela loja. A loja fica para o que muda o código nativo, como uma permissão nova ou um módulo de sensor.
Uma API em Workers
A API é um Cloudflare Worker escrito com Hono. Um Worker não é um servidor ligado esperando pedidos: é uma função que roda perto de quem chama, em centenas de cidades, e escala sozinha. Não há máquina para atualizar nem "quantas instâncias" para decidir.
Todas as regras moram na API: preço da corrida, quem recebe a oferta, quanto o motorista ganha, o que acontece quando alguém cancela. Os apps mostram e pedem; quem decide é o servidor. Isso evita a classe inteira de bugs em que o app mostra um preço e o servidor cobra outro.
Regras compartilhadas, testadas uma vez
Algumas contas precisam dar o mesmo resultado no celular e no servidor: a tarifa estimada, a divisão da comissão, as regras de navegação. Elas ficam num pacote próprio de JavaScript puro, sem dependência de tela ou de banco, usado pelos apps e pela API, e com testes próprios.
D1: o banco
Os dados ficam no D1, o banco SQLite gerenciado da Cloudflare. SQLite pode soar pequeno para um app de corridas, mas o nosso volume cabe com folga, e ganhamos transações de verdade e SQL comum. O banco tem 59 migrações versionadas no repositório, cada uma aplicada antes da versão da API que depende dela, nunca junto.
O D1 guarda um histórico de 30 dias e permite voltar o banco a qualquer minuto desse período. Além disso, uma tarefa semanal copia todas as tabelas para um arquivo comprimido, guardado à parte.
Tempo real com Durable Objects
Mostrar o carro andando no mapa do passageiro é o tipo de coisa que costuma pedir um servidor de WebSocket dedicado. Nós usamos Durable Objects: pequenos objetos com estado, cada um com um "endereço" único, que a Cloudflare mantém vivos enquanto são usados.
Temos dois tipos:
- Um por corrida. Recebe cada posição do motorista e a repassa ao passageiro por WebSocket. Entre uma mensagem e outra, o objeto "dorme" sem custo (WebSocket hibernável), e não há consulta repetida ao servidor.
- Um por motorista. Guarda a hora da última posição e mantém um único alarme. Se 20 segundos passam sem posição, o motorista está com "sinal perdido", e o app do passageiro para de fingir que o carro anda. Contamos isso em detalhe no terceiro artigo.
Tarefas agendadas
Muita coisa num app de corridas acontece sem ninguém pedir: ampliar a busca quando nenhum motorista aceitou, cancelar um pedido cujo Pix não foi pago, ativar uma corrida agendada 45 minutos antes, repassar o ganho do motorista, tirar do ar quem está com documento vencido. Tudo isso roda num gatilho de cron a cada minuto dentro do próprio Worker. As tarefas mais pesadas rodam a cada cinco minutos.
Cada tarefa é uma função comum, testável sozinha. E cada execução grava um "batimento": se o cron parar, um endereço de saúde passa a responder erro e um monitor externo avisa. O alerta não pode depender da coisa que ele vigia.
Como garantimos que funciona
- Testes no servidor contra um SQLite de verdade. Cada teste da API sobe um banco novo, aplica as mesmas migrações da produção e roda as rotas de verdade. São mais de 400 testes; eles cobrem desde "dois motoristas aceitam ao mesmo tempo: só um vence" até "a resposta do banco se perdeu: não transferir duas vezes".
- Galerias de tela. Cada tela dos apps pode ser aberta no navegador com dados de exemplo. Assim revisamos o desenho antes de levar ao celular.
- O celular de verdade. Toda mudança de app passa por um aparelho físico antes de ser dada como pronta: o GPS, a bateria e as notificações só se comportam de verdade lá.
- Rotas fechadas por padrão. Depois de cada publicação, chamamos cada rota nova sem login e esperamos a recusa.
O que custou
Nenhuma escolha é de graça:
- O D1 é um banco por vez. Para o nosso tamanho é ótimo; se um dia precisarmos de muito mais escrita, teremos que dividir os dados por região ou por tipo.
- Workers têm limite de tempo de processamento. Nada pode demorar muito: o que é longo vira várias execuções do cron, cada uma retomando de onde a anterior parou.
- Durable Objects mudam o jeito de pensar. O estado da corrida ao vivo mora num objeto, e o histórico mora no banco. Precisamos decidir com cuidado o que vai para cada lugar.
- React Native não é "escreva uma vez". GPS em segundo plano, economia de bateria e telas dobráveis pediram código e testes específicos de Android.
Considerações finais
A arquitetura do Carioca Taxi cabe numa frase: três apps, uma API, e tudo o mais na borda da rede. Não temos servidor para manter, e cada peça foi escolhida para ser a mais simples que resolve o problema de verdade.
Nos próximos artigos, entramos em duas partes em que "simples" não bastou: como fizemos o dinheiro do motorista à prova de falhas de rede, e o que aprendemos sobre GPS em segundo plano no Android.