Diario di bordo · Fase 0 · 3/7
Raccogliere indirizzi email senza una tabella locale
Le iscrizioni sono conservate da aubia.cloud. Il sito mantiene sessioni e una cache Redis, con richieste firmate e un form che dipende dalla rete.
di Mike, fondatore di Aubia Pubblicato il Aggiornato il 7 min di lettura
- architecture
- security
- waitlist
- gdpr



aubia.dev riceve le iscrizioni alla lista d'attesa senza conservarle in una tabella locale. Il repository del sito pubblico non contiene né un model Eloquent né una migration.
Ho affidato ad aubia.cloud la conservazione delle iscrizioni. Il sito pubblico valida il form, poi inoltra l'indirizzo email tramite HTTP. Il cloud conserva l'iscrizione, invia il link di conferma e gestisce il clic che riceve.
Questa separazione mi evita di mantenere un secondo database delle iscrizioni. Rende il form dipendente dalla rete e non mi esonera dal proteggere i dati gestiti dal sito. Le sessioni e la cache Redis rientrano in questa responsabilità.
Contenuto versionato con il codice
I testi del sito pubblico sono archiviati in file JSON e gli articoli in file Markdown. Il repository contiene tutte e sei le lingue disponibili sul sito. Una correzione del testo viene rilasciata con lo stesso processo di deploy di una correzione del codice.
In produzione, il blog legge un catalogo locale compilato durante il build. Contiene titoli, date e l'HTML generato da Markdown e Phiki, anche per gli articoli programmati. Un catalogo assente, non valido o obsoleto produce una risposta 503 sulle route che ne dipendono; durante la richiesta non viene eseguito un rendering Phiki di ripiego. Il parsing con cache Redis resta disponibile in locale e nei test, con voci che scadono dopo 24 ore.
La data di pubblicazione viene verificata dopo la lettura del catalogo, a ogni richiesta. Un articolo programmato già compilato diventa quindi accessibile alla data prevista senza un nuovo deploy. La presenza del suo HTML nel catalogo non lo rende pubblico prima di quella data.
Redis conserva anche le sessioni e i contatori usati dal rate limiter, che limita la frequenza delle richieste. Il database delle iscrizioni è ospitato in aubia.cloud.
I campi inviati al cloud
Il FormRequest valida i campi e costruisce un DTO, un oggetto tipizzato che trasporta i dati. Il controller passa questo oggetto all'Action SubmitWaitlistEmailAction.
Il corpo in uscita contiene l'indirizzo email, la lingua, i parametri UTM della campagna originale e il paese del visitatore, letto nell'header che Cloudflare aggiunge alla sua richiesta. Il timestamp del controllo del ritardo minimo serve alla validazione locale e non è incluso nel DTO.
Lo stato restituito dall'Action determina il messaggio mostrato dopo il redirect:
// Estratto da WaitlistController.
$status = $action->execute($request->toData());
if ($status->isSuccess()) {
return back()->with('waitlist_success', $status->messageKey());
}
return back()->with('waitlist_error', $status->messageKey());
Il messaggio flash viene conservato nella sessione per la richiesta successiva. Contiene una chiave di traduzione del risultato, senza copiare l'indirizzo email.
Un errore di validazione richiede un'ulteriore precauzione. Di norma Laravel conserva nella sessione i valori precedenti del form per mostrarli di nuovo. Il sito pubblico esclude esplicitamente l'indirizzo email da questo meccanismo:
// bootstrap/app.php, configurazione delle eccezioni.
$exceptions->dontFlash(['email']);
Una firma sul corpo esatto
L'Action serializza i campi in JSON, poi calcola un HMAC SHA-256 usando una chiave condivisa. Questa firma consente al cloud di verificare il corpo ricevuto con la stessa chiave.
In questo estratto, i nomi della chiave di configurazione e dell'header della firma sono illustrativi:
$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),
];
Il client HTTP invia esattamente il corpo firmato. La chiave non viene trasmessa in questa chiamata. L'header Origin indica l'origine prevista dal contratto; il mittente dichiara da sé quel valore.
HTTPS protegge il trasporto. L'HMAC verifica l'integrità del corpo e dimostra che il mittente conosce la chiave. Il form all'origine di una richiesta firmata può essere stato compilato tanto da un programma quanto da una persona, e la richiesta può essere ripetuta in seguito.
Honeypot e ritardo minimo filtrano una parte degli invii automatizzati del form. Un bot può comunque aggirarli adattando i propri campi o aspettando prima dell'invio. La firma e questi filtri rispondono a problemi diversi.
In produzione, il sito pubblico rifiuta di inviare la richiesta se la firma è disattivata o la chiave manca. L'impostazione che consente un invio non firmato in locale vale solo per il sito pubblico: non disattiva la verifica lato cloud.
L'incertezza dopo un errore di rete
Il client CloudApiClient gestisce queste due chiamate in modo diverso. Il GET del contatore può essere ritentato dopo un errore di rete o una risposta 5xx. Rileggere il totale non crea un'iscrizione.
Le risposte 4xx non attivano alcun nuovo tentativo, compresa la risposta 429 di rate limiting. Ripetere subito una richiesta rifiutata perché il servizio è saturo aggiungerebbe carico.
Per il POST, solo una ConnectionException autorizza un nuovo tentativo. Non appena riceve una risposta HTTP, anche se è un errore server, il client la restituisce all'Action senza ripetere la chiamata.
L'assenza di una risposta lascia comunque incerto l'esito. Un timeout può verificarsi dopo che l'indirizzo è stato memorizzato, ma prima che la risposta raggiunga il sito pubblico.
Il cloud impone l'unicità dell'indirizzo email nel database e coordina le iscrizioni concorrenti tramite transazioni e lock. Se due creazioni entrano in conflitto con questo vincolo di unicità, l'elaborazione rilegge l'iscrizione esistente.
Queste protezioni impediscono una seconda riga per lo stesso indirizzo. Gli altri passaggi dell'elaborazione possono essere eseguiti più di una volta: un nuovo tentativo del job di invio può reinviare l'email.
Il link di conferma ricevuto dal sito
Il cloud crea il token e invia un link a una pagina di aubia.dev. Il link include il token nella query string, quindi il server web lo riceve al caricamento iniziale.
Quando React è attivo, la pagina legge il token nel browser e chiama direttamente il cloud per confermare l'iscrizione. La disiscrizione segue lo stesso principio. Queste chiamate non passano dal proxy usato per inviare l'indirizzo email.
La visita iniziale lascia anche una traccia tecnica lato web. Laravel conserva l'URL completo come URL precedente nella sessione, e Inertia lo include nei dati della pagina.
Il link consente di confermare un'iscrizione in attesa entro 48 ore. Il cloud pianifica ogni giorno la cancellazione degli indirizzi che restano non confermati oltre 30 giorni, come descritto nell'informativa sulla privacy. Il cloud può reinviare un'email di conferma dopo un nuovo invio del form. Le email di benvenuto e di lancio attendono la conferma.
Il percorso di iscrizione alla lista d'attesa descrive in dettaglio il form, la conferma e il contatore.
Sessioni e preferenze del visitatore
Il rate limiter conta le richieste in base all'indirizzo IP e a un'impronta dell'indirizzo email, con un ulteriore limite complessivo. Le chiavi conservate sono sottoposte a hash e scadono con la relativa finestra di limitazione. Queste impronte non bastano a rendere anonimi i dati.
Il contenuto editoriale di una pagina localizzata è condiviso da tutti i visitatori. La risposta completa varia in base ai messaggi flash, agli errori del form e ai dati specifici della richiesta.
La route radice / sceglie la lingua in base a un cookie di preferenza e poi all'header Accept-Language. I cookie di sessione e di protezione CSRF accompagnano il cookie di lingua. La preferenza del tema viene conservata separatamente nel localStorage, senza essere inviata automaticamente al server.
Anche i dati diagnostici richiedono un periodo di conservazione controllato. I backup e la cancellazione delle iscrizioni sono gestiti nel cloud.
Un'iscrizione dipendente dalla rete
Se il cloud non risponde, il sito pubblico segnala un errore. Non conserva un'iscrizione locale né la mette in coda per inviarla in seguito. Dopo un timeout, l'indirizzo potrebbe comunque essere stato memorizzato nel cloud; un nuovo invio passa quindi dalla logica di gestione dei duplicati.
Il contatore ha un fallback diverso. Il sito pubblico conserva l'ultimo valore noto per un periodo limitato e può mostrarlo durante un'interruzione. Se non è disponibile alcun valore, il conteggio viene omesso.
Devo ruotare la chiave condivisa su entrambi i lati e mantenere disponibile l'API. Un'applicazione che dovesse accettare iscrizioni offline richiederebbe un'architettura diversa. Su aubia.dev, aubia.cloud gestisce l'archiviazione delle iscrizioni, la conferma e la cancellazione.
La beta di Aubia è gratuita e senza limiti di tempo. Per ricevere il suo invito basta iscriversi.
Si iscriva alla lista d'attesa