Vai al contenuto principale

Diario di bordo · Fase 0 · 4/7

L'iscrizione alla lista d'attesa, da un capo all'altro

Form ottimista React 19, anti-bot senza captcha, quattro stati per un POST e un contatore che conta solo i confermati: il percorso di iscrizione di Aubia, compromesso per compromesso.

Pubblicato il 3 agosto 20266 min di lettura
  • waitlist
  • react
  • security
  • gdpr

L'articolo precedente ha mostrato che aubia.dev non ha alcun database, e che l'iscrizione parte verso aubia.cloud attraverso un proxy firmato. Qui risalgo di un gradino per raccontare l'intero percorso, dal campo email fino all'email di conferma, e soprattutto i compromessi dietro ogni decisione. Un'iscrizione a una lista d'attesa sembra banale. Ne ho fatto il pezzo più curato del sito.

Il flusso animato del double opt-in: POST del visitatore verso aubia.dev, passaggio firmato HMAC verso api.aubia.cloud, email con token monouso, clic di conferma entro 48 ore.

Un form ottimista

Il form è un componente React 19 collegato a Inertia. Nulla si ricarica: all'invio, un segnale ottimista parte verso un toast globale, costruito con l'hook useOptimistic di React 19. L'interfaccia conferma immediatamente, poi la vera risposta del server riconcilia lo stato, senza fare nulla in caso di successo e annullando la visualizzazione in caso di errore.

Un dettaglio cura l'input: nel momento in cui lascia il campo, una routine di suggerimento individua i refusi comuni nel dominio e propone una correzione cliccabile, quella che trasforma gmial.com in gmail.com. Nulla di tutto ciò è cosmetico. Ogni email digitata male è un'iscrizione persa che non riceverà mai la sua conferma.

L'accessibilità segue la stessa esigenza: un'etichetta associata, aria-invalid sul campo in errore, role="alert" sui messaggi, e un suggerimento collegato tramite aria-describedby.

Anti-bot senza attrito: honeypot invece di captcha

Niente captcha, per cominciare. Un captcha protegge dai bot tassando ogni umano con un fastidio, e porta un terzo attore nel percorso. Ho preferito due trappole invisibili. La prima è un campo esca, nascosto in CSS, che solo un bot compila e che il server rifiuta se non è vuoto. La seconda è una trappola temporale: un timestamp impostato al montaggio della pagina, confrontato con l'istante dell'invio. Un bot che invia subito dopo il caricamento si tradisce da solo.

public function rules(): array
{
    return [
        'email' => ['required', 'email:rfc', 'max:254'],
        '_fax' => ['prohibited'],              // campo esca, deve restare vuoto
        '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', 'Invio troppo rapido.');
        }
    });
}

Un throttle nominato completa il dispositivo sul versante server, con tre limiti simultanei: uno per indirizzo IP, uno più severo per indirizzo email, e un tetto globale che assorbe un attacco distribuito. Lei non vede nulla di tutto questo. Un bot, invece, si scontra con tutti e tre in un colpo solo.

Quattro stati per un solo POST

Una sola e identica azione, iscriversi, copre diverse situazioni reali. Il cloud le distingue e il sito le traduce in messaggi precisi, invece di un vago «è stato inviato». Il proxy legge lo stato restituito da aubia.cloud e lo mappa a un enum di business.

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,
};

Quattro esiti di successo: una nuova iscrizione, un rinvio della conferma dopo un ritardo, un'iscrizione già in attesa, un'iscrizione già confermata. Ciascuno merita il proprio messaggio, perché un visitatore che ha già confermato e si iscrive di nuovo non deve pensare di aver appena ricominciato da capo.

Il double opt-in, o perché preferisco una lista più piccola

Arriva la scelta che struttura tutto il resto. Avrei potuto registrare ogni email e considerare l'iscrizione conclusa. Ho scelto il double opt-in: il cloud invia un'email con un link di conferma monouso, valido per quarantotto ore, e finché quel link non viene cliccato, nessuna comunicazione parte. Le iscrizioni non confermate vengono eliminate dopo trenta giorni.

Questa scelta riduce deliberatamente la lista. È proprio questo il punto. Una lista di mille indirizzi realmente confermati vale più di una lista di cinquemila di cui non so nulla. Prova un consenso datato, valida che l'indirizzo esiste, e garantisce che al lancio della beta 0.1 io stia parlando con persone che hanno davvero alzato la mano. La qualità della lista conta più del suo volume esibito.

Una conferma con diversi esiti, tenuta fuori dall'indice

Il clic sul link porta a una pagina dedicata di aubia.dev. Legge il token dall'URL, interroga il cloud e mostra uno di cinque esiti: confermato, già confermato, link non valido, non trovato, o scaduto. Questa pagina, come quella di disiscrizione, è esplicitamente rimossa dall'indice dei motori di ricerca.

<SeoHead
    path="/waitlist/confirmed"
    robots="noindex"
    title={t('waitlist.confirmed.seo_title')}
/>

Queste URL portano un token monouso e non hanno alcun valore nella ricerca. Escluderle dall'indice impedisce a un link di conferma di indugiare nei risultati e protegge la pulizia del posizionamento. La disiscrizione, dal canto suo, avviene con un clic da ogni email, in linea con lo standard RFC 8058, e la sua logica vive sul versante cloud, mai sul sito.

La sicurezza di questa pagina si regge sulla semantica del token stesso: monouso, hashato sul versante cloud, cancellato dopo il consumo, scaduto dopo quarantotto ore.

Un contatore che conta solo i confermati

Resta la riprova sociale. La landing mostra un numero di iscritti, ma conta solo le persone che hanno confermato, mai gli indirizzi in attesa. E non si mostra finché non viene raggiunta una soglia minima: al di sotto, il componente non renderizza nulla. Meglio non mostrare alcun numero che mostrarne uno basso e scoraggiante all'inizio della campagna. Un contatore onesto non può giocare su due tavoli. O gonfia il numero con le iscrizioni non confermate e mente, oppure mostra solo ciò che è reale.

Tutta la logica di iscrizione pende dalla stessa parte: il reale, misurato con sobrietà, e nulla che non sia dimostrabile. Sul versante GDPR, si riduce a una frase sotto il form, una sola finalità, l'invito alla beta, e nessun dato personale a riposo sul sito.

Tutto questo per un campo email

Questo percorso mobilita diversi mattoni: un form, una validazione, un enum di stati, un proxy, un contatore. Su un repo così piccolo, tutto ciò merita di essere disposto con metodo invece che sparso. Quell'organizzazione in moduli, gliela apro subito dopo.

Per essere avvisato all'apertura della beta 0.1, si iscriva alla lista d'attesa.

Questo cantiere si racconta qui, articolo dopo articolo. Salite a bordo: i vostri riscontri disegneranno il seguito.

Iscriviti alla lista d'attesa