Vai al contenuto principale

Diario di bordo · Fase 0 · 3/7

Un sito vetrina senza database

Zero tabelle, zero dati personali a riposo: la lista d'attesa di aubia.dev parte verso aubia.cloud tramite un proxy firmato HMAC, con un retry che non raddoppia mai un'iscrizione.

Pubblicato il 27 luglio 20266 min di lettura
  • architecture
  • security
  • waitlist
  • gdpr

La landing di Aubia raccoglie indirizzi email. Verrebbe da pensare che serva quindi un database. Non ne ha nessuno. Nessuna tabella, nessun modello Eloquent, nemmeno un indirizzo email scritto su disco sul versante sito. Non ho subìto questa assenza, l'ho posta fin dal primo commit. Una pagina che iscrive delle persone a una lista d'attesa può non persistere nulla, ed è più semplice così.

Un sito vetrina non ha bisogno di un database

Tutto il contenuto di aubia.dev è statico per natura: il testo di marketing vive in file di traduzione JSON, gli articoli di questo blog in file Markdown versionati insieme al codice. Nulla di tutto ciò giustifica un database. L'unico flusso dinamico della Fase 0, l'iscrizione alla lista d'attesa, non memorizza nulla localmente: passa verso aubia.cloud.

Il repo web non porta quindi alcun modello Eloquent, nessuna migrazione di dati di business. Ciò che tocca Redis si limita alla cache transitoria e alla sessione: un contatore, un token anti-abuso, mai un indirizzo email durevole.

Togliere il database da un sito vetrina significa rimuovere in un colpo solo un'intera classe di preoccupazioni: backup, cifratura a riposo, violazione di dati, cancellazione GDPR. Non si mette in sicurezza ciò che non si detiene.

Il confine tra aubia.dev, il sito pubblico senza stato, e api.aubia.cloud che detiene lo storage della lista d'attesa, i token e la traccia del consenso; solo un POST firmato HMAC lo attraversa.

Zero dati personali memorizzati sul versante web

La regola è assoluta: tutti i dati personali passano dal cloud, mai da un archivio locale sul versante sito. Quando invia la sua email, il controller resta deliberatamente sottile. Valida, costruisce un oggetto di trasporto tipizzato, delega a un'Action e restituisce un messaggio flash. Non sa nulla della rete, della firma o del database, perché non ce n'è.

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());
    }
}

L'email attraversa la memoria del worker per la durata di una sola chiamata HTTP in uscita, poi svanisce. Nulla resta indietro.

Un proxy firmato verso aubia.cloud

Il modulo Waitlist è entrato nel repo all'inizio di maggio 2026, una volta preparata la scena della landing. La sua Action SubmitWaitlistEmail fa da ponte. Serializza il payload, calcola una firma HMAC SHA-256 e la invia in POST ad aubia.cloud. La firma prova al cloud che la richiesta proviene davvero dal sito ufficiale, e l'origine è vincolata nel contratto.

$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);
}

Ho tenuto deliberatamente minima questa firma: copre il corpo JSON grezzo, senza timestamp né nonce anti-replay. Il sito è l'unico chiamante legittimo e la sua origine è ristretta per IP sul versante cloud, il che rende poco realistico un attacco replay nella Fase 0. Aggiungere un meccanismo anti-replay avrebbe appesantito il contratto senza alcun beneficio tangibile a questo stadio.

In produzione, questa firma non può essere disattivata: il codice lancia un'eccezione se qualcuno prova a spegnerla. Un dettaglio conta nel payload inviato. Il form intrappola i bot con un honeypot temporale, un campo started_at impostato quando la pagina si monta. Questo campo serve solo alla validazione locale e non viene mai trasmesso al cloud, il che mantiene minima la superficie del contratto e stabilizza la firma.

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
}

La firma non è lì per sostituire le trappole anti-bot ma per aggiungere un livello sopra di esse. Un honeypot o un ritardo minimo si aggirano con un bot un po' accorto, capace di leggere il DOM o di programmare un'attesa. L'HMAC, invece, rende costoso lo scraping di massa: senza il segreto condiviso, nessuna richiesta contraffatta viene accettata.

Un retry che non raddoppia mai un'iscrizione

La rete a volte fallisce. Un client HTTP ingenuo ripeterebbe allora la richiesta, con il rischio di iscrivere la stessa persona due volte. Perciò il client in uscita distingue due casi in base all'idempotenza del verbo. Un GET, come la lettura del contatore, può essere ripetuto senza effetti collaterali. Un POST di iscrizione no: ripete solo se la richiesta non ha mai raggiunto il 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 il cloud ha risposto, anche con un errore server, il sito non ritenta: mappa la risposta a uno stato di business e mostra il messaggio giusto. La correzione sta nella semantica del retry, non nell'affidarsi al caso.

Il double opt-in vive sul versante cloud

È aubia.cloud a gestire il double opt-in: generazione del token di conferma, invio dell'email, scadenza dopo 48 ore, e soprattutto la traccia del consenso. Finché un'iscrizione non è confermata, nessuna comunicazione parte, e gli indirizzi non confermati vengono eliminati automaticamente.

Il sito non vede mai nulla di tutto ciò. Non conosce né il token, né lo stato di conferma, né alcun dato durevole. Questo confine netto tra un front-end pubblico senza stato e un cloud che detiene la logica e i dati è ciò che rende il tutto semplice da ragionare e da mettere in sicurezza.

Cosa semplifica l'assenza di un database

Senza un database locale, distribuire aubia.dev diventa banale: nessuna migrazione da eseguire, nessuna connessione da provisionare, nessuno stato da replicare tra i worker. La sicurezza si riduce di conseguenza, perché il sito non detiene alcun segreto utente da proteggere. E la conformità GDPR si sposta in un unico luogo, il cloud, invece di essere sparsa su ogni superficie.

Un sito vetrina deve informare e convertire, non diventare una cassaforte. Togliendogli il database, gli tolgo le sue responsabilità più pesanti.

Questo proxy firmato è solo un anello. L'intero percorso, però, merita il proprio racconto: il form, l'anti-bot senza captcha, il double opt-in e il contatore, con i compromessi dietro ciascuna di queste scelte.

Per seguire la costruzione di Aubia ed 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