Devlog · Fase 0 · 4/7
A inscrição na lista de espera, de ponta a ponta
Formulário otimista React 19, anti-bot sem captcha, quatro status para um POST e uma contagem que só conta os confirmados: o percurso de inscrição de Aubia, escolha por escolha.
- waitlist
- react
- security
- gdpr

O artigo anterior mostrou que aubia.dev não tem banco de dados, e que a inscrição sai para aubia.cloud por um proxy assinado. Aqui, subo um degrau para contar o percurso completo, do campo de e-mail até o e-mail de confirmação, e sobretudo os trade-offs por trás de cada decisão. Uma inscrição em lista de espera parece trivial. Fiz dela o trecho mais bem-cuidado da vitrine.
Um formulário otimista
O formulário é um componente React 19 conectado ao Inertia. Nada recarrega: no envio, um sinal otimista parte para um toast global, construído com o hook useOptimistic do React 19. A interface confirma imediatamente, depois o retorno real do servidor reconcilia o estado, sem fazer nada em caso de sucesso e revertendo a exibição em caso de erro.
Um detalhe cuida da digitação: no momento em que você sai do campo, uma rotina de sugestão detecta os erros de digitação comuns no domínio e propõe uma correção clicável, do tipo gmial.com corrigido para gmail.com. Nada disso é cosmético. Cada e-mail mal digitado é uma inscrição perdida que nunca receberá a sua confirmação.
A acessibilidade segue a mesma exigência: label associado, aria-invalid no campo em erro, role="alert" nas mensagens, e uma sugestão vinculada por aria-describedby.
Anti-bot sem atrito: honeypot em vez de captcha
Nada de captcha, para começar. Um captcha protege dos bots taxando cada humano com uma tarefa chata, e traz um terceiro para dentro do percurso. Preferi duas armadilhas invisíveis. A primeira é um campo isca, escondido em CSS, que só um bot preenche e que o servidor rejeita se não estiver vazio. A segunda é uma armadilha de tempo: um timestamp posto na montagem da página, comparado com o instante do envio. Um bot que envia logo após o carregamento se entrega sozinho.
public function rules(): array
{
return [
'email' => ['required', 'email:rfc', 'max:254'],
'_fax' => ['prohibited'], // campo isca, deve permanecer vazio
'rendered_at' => ['required', 'integer', 'min:1'],
'locale' => ['required', 'in:fr,en,es,de,it,pt'],
// ... utm nullable
];
}
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator): void {
$nowMs = (int) (microtime(true) * 1000);
if (($nowMs - (int) $this->input('rendered_at')) < self::MIN_FILL_DURATION_MS) {
$validator->errors()->add('rendered_at', 'Envio rápido demais.');
}
});
}
Um throttle nomeado completa o dispositivo do lado do servidor, com três limites simultâneos: um por endereço IP, um mais estrito por endereço de e-mail, e um teto global que absorve um ataque distribuído. Você não vê nada disso. Um bot, esse, se choca com os três de uma vez.
Quatro status para um único POST
Uma mesma ação, inscrever-se, recobre várias situações reais. O cloud as distingue e a vitrine as traduz em mensagens precisas, em vez de um vago "foi enviado". O proxy lê o status retornado por aubia.cloud e o mapeia para um enum de negócio.
return match (true) {
$httpStatus >= 500 => self::CloudDown,
$cloudStatus === 'confirmation_sent' => self::ConfirmationSent,
$cloudStatus === 'confirmation_resent' => self::ConfirmationResent,
$cloudStatus === 'already_pending' => self::AlreadyPending,
$cloudStatus === 'already_confirmed' => self::AlreadyConfirmed,
default => self::CloudDown,
};
Quatro desfechos de sucesso: uma nova inscrição, um reenvio de confirmação após um atraso, uma inscrição já pendente, uma inscrição já confirmada. Cada um merece sua própria mensagem, porque um visitante que já confirmou e se inscreve de novo não deve pensar que acabou de recomeçar do zero.
O double opt-in, ou por que prefiro uma lista menor
Vem a escolha que estrutura todo o resto. Eu poderia ter registrado cada e-mail e considerado a inscrição feita. Escolhi o double opt-in: o cloud envia um e-mail com um link de confirmação de uso único, válido por quarenta e oito horas, e enquanto esse link não é clicado, nenhuma comunicação sai. As inscrições não confirmadas são purgadas após trinta dias.
Essa escolha encolhe a lista deliberadamente. É esse o propósito. Uma lista de mil endereços realmente confirmados vale mais do que uma lista de cinco mil dos quais não sei nada. Ela prova um consentimento datado, valida que o endereço existe, e garante que, no lançamento da beta 0.1, eu falo com pessoas que de fato levantaram a mão. A qualidade da lista importa mais do que seu volume exibido.
Uma confirmação com vários desfechos, fora do índice
O clique no link chega a uma página dedicada de aubia.dev. Ela lê o token na URL, consulta o cloud, e exibe um de cinco desfechos: confirmado, já confirmado, link inválido, não encontrado, ou expirado. Essa página, como a de cancelamento de inscrição, é explicitamente retirada do índice dos motores.
<SeoHead
path="/waitlist/confirmed"
robots="noindex"
title={t('waitlist.confirmed.seo_title')}
/>
Essas URLs carregam um token de uso único e não têm valor algum em busca. Excluí-las do índice evita que um link de confirmação fique perdido em resultados e protege a limpeza do ranqueamento. O cancelamento de inscrição, por sua vez, acontece em um clique a partir de cada e-mail, em conformidade com a norma RFC 8058, e sua lógica vive do lado do cloud, nunca na vitrine.
A segurança dessa página repousa na semântica do próprio token: uso único, hasheado do lado do cloud, apagado após o consumo, expirado ao fim de quarenta e oito horas.
Uma contagem que só conta os confirmados
Resta a prova social. A landing exibe um número de inscritos, mas ele só conta as pessoas que confirmaram, nunca os endereços pendentes. E ele não se exibe enquanto um limite mínimo não é atingido: abaixo disso, o componente não renderiza nada. Melhor não exibir número algum do que exibir um baixo e desencorajador no início da campanha. Uma contagem honesta não pode jogar dos dois lados. Ou ela infla o número com inscrições não confirmadas e mente, ou mostra apenas o real.
Toda a lógica de inscrição pende para o mesmo lado: o real, medido com sobriedade, e nada que não seja comprovável. Do lado do RGPD, isso se resume a uma frase sob o formulário, uma finalidade única, o convite para a beta, e nenhum dado pessoal em repouso na vitrine.
Tudo isso para um campo de e-mail
Esse percurso mobiliza vários blocos: um formulário, uma validação, um enum de status, um proxy, uma contagem. Em um repo tão pequeno, tudo isso merece ser arrumado com método em vez de disperso. Essa organização em módulos, eu a abro para você logo em seguida.
Para ser avisado quando a beta 0.1 abrir, entre na lista de espera.
Esta construção é contada aqui, artigo após artigo. Embarque: o seu feedback desenhará o que vem a seguir.
Entrar na lista de espera