Devlog · Fase 0 · 3/7
Coletar endereços de e-mail sem uma tabela local
As inscrições são armazenadas em aubia.cloud. O site mantém sessões e um cache Redis, com requisições assinadas e um formulário que depende da rede.
por Mike, fundador da Aubia Publicado em Atualizado em 7 min de leitura
- architecture
- security
- waitlist
- gdpr



aubia.dev recebe inscrições para a lista de espera sem armazená-las em uma tabela local. O repositório do site público não contém nem um model Eloquent nem uma migration.
Deixei o armazenamento das inscrições sob responsabilidade de aubia.cloud. O site público valida o formulário e depois encaminha o endereço de e-mail por HTTP. O cloud armazena a inscrição, envia o link de confirmação e processa o clique que recebe.
Essa separação me poupa de manter um segundo banco de dados de inscrições. Ela torna o formulário dependente da rede, mas não me dispensa de proteger os dados tratados pelo site. As sessões e o cache Redis fazem parte dessa responsabilidade.
Conteúdo versionado junto com o código
Os textos do site público ficam em arquivos JSON, e os artigos, em arquivos Markdown. O repositório contém os seis idiomas oferecidos pelo site. Uma correção de texto é publicada no mesmo deploy que uma correção de código.
Em produção, o blog lê um catálogo local compilado durante o build. Ele contém títulos, datas e o HTML gerado por Markdown e Phiki, inclusive para os artigos agendados. Um catálogo ausente, inválido ou desatualizado produz uma resposta 503 nas rotas que dependem dele; não há renderização Phiki de fallback durante a requisição. O parsing com cache Redis continua disponível localmente e nos testes, com entradas que expiram após 24 horas.
A data de publicação é verificada depois da leitura do catálogo, em cada requisição. Um artigo agendado já compilado fica acessível na data prevista sem um novo deploy. A presença de seu HTML no catálogo não o torna público antes dessa data.
O Redis também armazena as sessões e os contadores usados pelo rate limiter, que limita a frequência das requisições. O banco de dados das inscrições fica em aubia.cloud.
Campos enviados ao cloud
O FormRequest valida os campos e cria um DTO, um objeto tipado que transporta os dados. O controller passa esse objeto para a Action SubmitWaitlistEmailAction.
O corpo enviado contém o endereço de e-mail, o idioma, os parâmetros UTM da campanha original e o país do visitante, lido no cabeçalho que o Cloudflare acrescenta à sua requisição. A marca temporal da verificação do atraso mínimo serve à validação local e não faz parte do DTO.
O status retornado pela Action determina a mensagem exibida após o redirecionamento:
// Trecho de WaitlistController.
$status = $action->execute($request->toData());
if ($status->isSuccess()) {
return back()->with('waitlist_success', $status->messageKey());
}
return back()->with('waitlist_error', $status->messageKey());
A mensagem flash fica na sessão para a próxima requisição. Ela contém uma chave de tradução para o resultado, sem copiar o endereço de e-mail.
Um erro de validação exige mais uma precaução. O Laravel normalmente mantém os valores anteriores do formulário na sessão para exibi-los novamente. O site público exclui explicitamente o e-mail desse mecanismo:
// bootstrap/app.php, configuração das exceções.
$exceptions->dontFlash(['email']);
A assinatura do corpo exato
A Action serializa os campos em JSON e depois calcula um HMAC SHA-256 usando uma chave compartilhada. Essa assinatura permite que o cloud verifique o corpo recebido com a mesma chave.
Neste trecho, os nomes da chave de configuração e do cabeçalho de assinatura são ilustrativos:
$rawBody = json_encode($data->toCloudPayload(), JSON_THROW_ON_ERROR);
$secret = (string) config('services.cloud.signing_key');
$headers = [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Origin' => 'https://aubia.dev',
'X-Signature' => 'sha256=' . hash_hmac('sha256', $rawBody, $secret),
];
O cliente HTTP envia exatamente o corpo que foi assinado. A chave em si não é enviada nessa chamada. O cabeçalho Origin indica a origem esperada pelo contrato. O próprio remetente declara o valor desse cabeçalho.
O HTTPS protege o transporte. O HMAC verifica a integridade do corpo e comprova que o remetente conhece a chave. O formulário de origem de uma requisição assinada pode ter sido preenchido por um programa ou por uma pessoa, e a requisição pode ser repetida mais tarde.
O honeypot e o atraso mínimo filtram algumas submissões automatizadas. Um bot ainda pode contorná-los adaptando seus campos ou esperando antes de enviar. A assinatura e esses filtros tratam de problemas diferentes.
Em produção, o site público não envia se a assinatura estiver desativada ou se a chave estiver ausente. A configuração que permite enviar sem assinatura em ambiente local se aplica apenas ao site público. Ela não desativa a verificação no cloud.
Incerteza após um erro de rede
O CloudApiClient trata essas duas chamadas de forma diferente. O GET do contador pode ser tentado novamente após um erro de rede ou uma resposta 5xx. Ler o total novamente não cria uma inscrição.
Respostas 4xx não acionam novas tentativas, inclusive a resposta 429 de rate limit. Repetir imediatamente uma requisição rejeitada porque o serviço está saturado aumentaria a carga.
No POST, apenas uma ConnectionException permite uma nova tentativa. Assim que uma resposta HTTP é recebida, mesmo que seja um erro do servidor, o cliente devolve a resposta à Action sem repetir a chamada.
A ausência de uma resposta ainda deixa o resultado incerto. Um timeout pode acontecer depois que o endereço foi armazenado, mas antes de a resposta chegar ao site público.
O cloud garante a unicidade do endereço de e-mail no banco de dados e coordena inscrições concorrentes com transações e locking. Se duas criações colidirem com essa restrição de unicidade, o processamento recupera a inscrição existente.
Essas proteções evitam uma segunda linha para o mesmo endereço. As outras etapas do processamento podem ser executadas mais de uma vez: uma nova tentativa do job de envio pode reenviar o e-mail.
O link de confirmação recebido pelo site
O cloud cria o token e envia um link para uma página em aubia.dev. O link inclui o token na query string, então o servidor web o recebe no carregamento inicial.
Quando o React está ativo, a página lê o token no navegador e chama o cloud diretamente para confirmar a inscrição. O cancelamento da inscrição segue o mesmo princípio. Essas chamadas não passam pelo proxy usado para enviar o endereço de e-mail.
A visita inicial também deixa um rastro técnico no lado web. O Laravel armazena a URL completa como a URL anterior na sessão, e o Inertia a inclui nos dados da página.
O link pode confirmar uma inscrição pendente por 48 horas. O cloud agenda um expurgo diário dos endereços que continuam sem confirmação após 30 dias, conforme descrito na política de privacidade. O cloud pode reenviar um e-mail de confirmação após um novo envio do formulário. Os e-mails de boas-vindas e de lançamento aguardam a confirmação.
O fluxo de inscrição na lista de espera detalha o formulário, a confirmação e o contador.
Sessões e preferências dos visitantes
O rate limiter contabiliza as requisições com base no endereço IP e em um fingerprint do endereço de e-mail, além de aplicar um limite geral. As chaves armazenadas são hasheadas e expiram junto com a janela do rate limit. Esses fingerprints não bastam para anonimizar os dados.
O conteúdo editorial de uma página localizada é compartilhado por todos os visitantes. A resposta completa varia de acordo com mensagens flash, erros do formulário e dados específicos da requisição.
A rota raiz / seleciona o idioma a partir de um cookie de preferência e depois do cabeçalho Accept-Language. O cookie de sessão e o cookie de proteção CSRF acompanham esse cookie de idioma. A preferência de tema é armazenada separadamente no localStorage, sem ser enviada automaticamente ao servidor.
Os dados de diagnóstico também precisam de um período de retenção controlado. Os backups e o expurgo da lista de espera são gerenciados no cloud.
Uma inscrição dependente da rede
Se o cloud não responder, o site público informa que houve uma falha. O site público não mantém uma inscrição local para enviá-la mais tarde. Depois de um timeout, o endereço pode ter sido armazenado no cloud mesmo assim. Um novo envio passa então pela lógica de tratamento de duplicatas.
O contador tem um fallback diferente. O site público mantém o último valor conhecido por um período limitado e pode exibi-lo durante uma indisponibilidade. Se nenhum valor estiver disponível, a contagem é omitida.
Preciso fazer a rotação da chave compartilhada nos dois lados e manter a API disponível. Uma aplicação que precise aceitar inscrições offline exigiria outro desenho. No aubia.dev, aubia.cloud cuida do armazenamento das inscrições, da confirmação e do expurgo delas.
A beta da Aubia é gratuita e sem limite de tempo. Inscreva-se para receber o seu convite.
Entrar na lista de espera