Ir para o conteúdo principal

Devlog · Fase 0 · 3/7

Uma vitrine sem banco de dados

Zero tabela, zero dado pessoal em repouso: a lista de espera de aubia.dev sai para aubia.cloud via um proxy assinado com HMAC, com um retry que nunca duplica uma inscrição.

Publicado em 27 julho 20266 min de leitura
  • architecture
  • security
  • waitlist
  • gdpr

A landing de Aubia coleta endereços de e-mail. Você poderia achar que ela, portanto, precisa de um banco de dados. Ela não tem nenhum. Nenhuma tabela, nenhum modelo Eloquent, nenhum endereço de e-mail escrito em disco do lado da vitrine. Não sofri essa ausência, eu a coloquei desde o primeiro commit. Uma página que inscreve pessoas em uma lista de espera pode não persistir nada, e é mais simples assim.

Uma vitrine não precisa de banco de dados

Todo o conteúdo de aubia.dev é estático por natureza: os textos de marketing vivem em arquivos de tradução JSON, os artigos deste blog em arquivos Markdown versionados junto com o código. Nada disso justifica um banco de dados. O único fluxo dinâmico da Fase 0, a inscrição na lista de espera, não armazena nada localmente: ele transita para aubia.cloud.

O repo web não embarca, portanto, nenhum modelo Eloquent, nenhuma migração de dado de negócio. O que toca o Redis se limita a cache transitório e à sessão: uma contagem, um token anti-abuso, nunca um endereço de e-mail durável.

Retirar o banco de dados de uma vitrine é retirar de uma vez toda uma classe de preocupações: backups, criptografia em repouso, vazamento de dados, purga RGPD. Você não protege o que não detém.

A fronteira entre aubia.dev, o site público sem estado, e api.aubia.cloud, que guarda o armazenamento da lista de espera, os tokens e o rastro do consentimento; apenas um POST assinado com HMAC a atravessa.

Zero dado pessoal armazenado do lado web

A regra é absoluta: todo dado pessoal passa pelo cloud, nunca por um armazenamento local do lado da vitrine. Quando você envia seu e-mail, o controller permanece deliberadamente enxuto. Ele valida, constrói um objeto de transporte tipado, delega a uma Action e retorna uma mensagem flash. Ele não conhece nem a rede, nem a assinatura, nem o banco de dados, porque não há nenhum.

final class WaitlistController
{
    public function store(SubmitWaitlistRequest $request, SubmitWaitlistEmail $action): RedirectResponse
    {
        $data = WaitlistSubmissionData::fromArray([
            'email' => $request->validated()['email'],
            'locale' => $request->validated()['locale'],
            // ... utm et honeypot temporel
        ]);

        $status = $action->execute($data);

        return $status->isSuccess()
            ? back()->with('waitlist_success', $status->messageKey())
            : back()->with('waitlist_error', $status->messageKey());
    }
}

O e-mail atravessa a memória do worker pelo tempo de uma chamada HTTP de saída, depois desaparece. Nada fica para trás.

Um proxy assinado para aubia.cloud

O módulo Waitlist entrou no repo no início de maio de 2026, uma vez montado o cenário da landing. Sua Action SubmitWaitlistEmail faz a ponte. Ela serializa o payload, calcula uma assinatura HMAC SHA-256 e faz um POST para aubia.cloud. A assinatura prova ao cloud que a requisição vem mesmo da vitrine oficial, e a origem está fixada no contrato.

$rawBody = json_encode($data->toCloudPayload(), JSON_THROW_ON_ERROR);

$headers = [
    'Content-Type' => 'application/json',
    'Origin' => 'https://aubia.dev',
];

if (config('services.cloud.waitlist_proxy_sign_hmac') !== false) {
    $secret = (string) config('services.cloud.waitlist_proxy_signing_key');
    $headers['X-Aubia-Signature'] = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
}

Mantive deliberadamente essa assinatura mínima: ela recai sobre o corpo JSON bruto, sem timestamp nem nonce anti-replay. A vitrine é o único chamador legítimo e sua origem é restrita por IP do lado do cloud, o que torna um ataque de replay pouco realista na Fase 0. Acrescentar um mecanismo anti-replay teria pesado o contrato sem benefício tangível neste estágio.

Em produção, essa assinatura não pode ser desativada: o código lança uma exceção se alguém tentar desligá-la. Um detalhe conta no payload enviado. O formulário armadilha os bots com um honeypot temporal, um campo started_at posto na montagem da página. Esse campo serve apenas à validação local e nunca é transmitido ao cloud, o que mantém a superfície do contrato mínima e estabiliza a assinatura.

public function toCloudPayload(): array
{
    return [
        'email' => $this->email,
        'utm_source' => $this->utmSource,
        'utm_medium' => $this->utmMedium,
        'utm_campaign' => $this->utmCampaign,
        'locale' => $this->locale,
    ];
    // started_at (honeypot) volontairement exclu
}

A assinatura não está ali para substituir as armadilhas anti-bot, mas para acrescentar uma camada por cima. Um honeypot ou um atraso mínimo podem ser contornados por um bot um pouco caprichado, capaz de ler o DOM ou de programar uma espera. O HMAC, por sua vez, torna o scraping em massa caro: sem o segredo compartilhado, nenhuma requisição forjada é aceita.

Um retry que nunca duplica uma inscrição

A rede falha às vezes. Um cliente HTTP ingênuo então reproduziria a requisição, ao risco de inscrever duas vezes a mesma pessoa. O cliente de saída distingue, portanto, dois casos conforme a idempotência do verbo. Um GET, como a leitura da contagem, pode ser reproduzido sem efeito colateral. Um POST de inscrição, não: ele só reproduz se a requisição nunca chegou ao cloud.

->retry(
    $retries + 1,
    fn(int $attempt): int => $backoffMs + random_int(0, 100),
    // Retry UNIQUEMENT sur erreur réseau : la requête n'a jamais atteint
    // le cloud. Jamais sur une réponse reçue, même 5xx (double inscription).
    fn(Throwable $e, PendingRequest $request): bool => $e instanceof ConnectionException,
    throw: false,
)

Se o cloud respondeu, mesmo com um erro de servidor, a vitrine não tenta de novo: ela mapeia a resposta para um status de negócio e exibe a mensagem certa. A correção vive na semântica do retry, não na adivinhação.

O double opt-in vive do lado do cloud

É aubia.cloud que cuida do double opt-in: geração do token de confirmação, envio do e-mail, expiração após 48 horas e, sobretudo, o registro do consentimento. Enquanto uma inscrição não é confirmada, nenhuma comunicação sai, e os endereços não confirmados são purgados automaticamente.

A vitrine nunca vê nada disso. Ela não conhece nem o token, nem o estado de confirmação, nem o menor dado durável. Essa fronteira nítida entre um frontend público sem estado e um cloud que detém a lógica e os dados é o que torna o conjunto simples de raciocinar e de proteger.

O que a ausência de banco de dados simplifica

Sem banco de dados local, o deploy de aubia.dev se torna trivial: nenhuma migração a rodar, nenhuma conexão a provisionar, nenhum estado a replicar entre workers. A segurança se reduz na mesma medida, porque a vitrine não detém nenhum segredo de usuário a proteger. E a conformidade RGPD se desloca para um único lugar, o cloud, em vez de estar espalhada por cada superfície.

Uma vitrine deve informar e converter, não se tornar um cofre. Ao lhe retirar o banco de dados, retiro dela as suas responsabilidades mais pesadas.

Esse proxy assinado é apenas um elo. O percurso completo, esse, merece o seu próprio relato: o formulário, o anti-bot sem captcha, o double opt-in e a contagem, com os trade-offs por trás de cada uma dessas escolhas.

Para acompanhar a construção de Aubia e 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