Journal de bord · Phase 0 · 3/7
Collecter des adresses email sans table locale
Les inscriptions sont enregistrées par aubia.cloud. La vitrine conserve des sessions et un cache Redis, avec des échanges signés et un formulaire dépendant du réseau.
par Mike, fondateur d'Aubia Publié le Mis à jour le 7 min de lecture
- architecture
- security
- waitlist
- gdpr



La vitrine aubia.dev reçoit les demandes d'inscription à la liste d'attente sans les enregistrer dans une table locale. Le dépôt de la vitrine ne contient ni modèle Eloquent ni migration.
J'ai confié le stockage des inscriptions à aubia.cloud. La vitrine valide le formulaire, puis transmet l'adresse email par HTTP. Le cloud conserve l'inscription, envoie le lien de confirmation et traite le clic reçu.
Cette séparation m'évite d'entretenir une seconde base d'inscriptions. Elle rend le formulaire dépendant du réseau et ne dispense pas de protéger les données traitées côté web. Les sessions et le cache Redis font partie de cette responsabilité.
Le contenu versionné avec le code
Les textes de la vitrine sont stockés dans des fichiers JSON, les articles dans des fichiers Markdown. Le dépôt comprend les six langues du site. Publier une correction de texte suit le même déploiement qu'une correction de code.
En production, le blog lit un catalogue local compilé au build. Il contient les titres, les dates et le HTML produit par Markdown et Phiki, y compris pour les articles programmés. Un catalogue absent, invalide ou périmé entraîne une réponse 503 sur les routes qui en dépendent ; aucun rendu Phiki de secours n'est lancé pendant la requête. Le parsing avec cache Redis reste disponible en local et dans les tests, avec une expiration après 24 heures.
La date de publication est vérifiée après lecture du catalogue, à chaque demande. Un article programmé déjà compilé devient donc accessible à la date prévue, sans attendre un déploiement. La présence de son HTML dans le catalogue ne le rend pas public avant cette date.
Redis conserve aussi les sessions et les compteurs du rate limiter, qui limite la fréquence des requêtes. La base des inscriptions se trouve dans aubia.cloud.
Les champs transmis au cloud
Le FormRequest valide les champs et construit un DTO, un objet typé qui transporte les données. Le controller transmet cet objet à l'Action SubmitWaitlistEmailAction.
Le corps envoyé comprend l'adresse email, la langue, les paramètres UTM de la campagne d'origine et le pays du visiteur, lu dans l'en-tête que Cloudflare ajoute à sa requête. L'horodatage du piège temporel sert à la validation locale et n'est pas inclus dans le DTO.
Le statut retourné par l'Action détermine le message affiché après la redirection :
// Extrait de WaitlistController.
$status = $action->execute($request->toData());
if ($status->isSuccess()) {
return back()->with('waitlist_success', $status->messageKey());
}
return back()->with('waitlist_error', $status->messageKey());
Ce message flash est conservé en session pour la requête suivante. Il contient une clé de traduction du résultat, sans recopier l'adresse email.
Une erreur de validation demande une précaution supplémentaire. Laravel conserve normalement les anciennes valeurs du formulaire en session pour permettre de les réafficher. La vitrine exclut explicitement l'email de ce mécanisme :
// bootstrap/app.php, configuration des exceptions.
$exceptions->dontFlash(['email']);
Une signature sur le corps exact
L'Action sérialise les champs en JSON, puis calcule un HMAC SHA-256 avec une clé partagée. Cette signature permet au cloud de vérifier le corps reçu avec la même clé.
Dans cet extrait, les noms de la clé de configuration et de l'en-tête de signature sont illustratifs :
$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),
];
Le client HTTP envoie exactement le corps qui a été signé. La clé elle-même n'est pas transmise dans cet appel. L'en-tête Origin indique l'origine prévue par le contrat ; l'émetteur déclare lui-même cette valeur.
HTTPS protège le transport. Le HMAC vérifie l'intégrité du corps et la connaissance de la clé. Le formulaire à l'origine d'une requête signée peut avoir été rempli par un programme comme par une personne, et la requête peut être rejouée plus tard.
Le honeypot et le délai minimal filtrent une partie des soumissions automatisées au formulaire. Ils restent contournables par un robot qui adapte ses champs ou attend avant l'envoi. La signature et ces filtres répondent à des problèmes différents.
En production, la vitrine interrompt l'envoi si la signature est désactivée ou la clé absente. Le réglage qui permet un envoi non signé en local concerne seulement la vitrine : il ne désactive pas la vérification du cloud.
L'incertitude après une erreur réseau
Le client CloudApiClient traite différemment ces deux appels. Le GET du compteur peut être retenté après une erreur réseau ou une réponse 5xx. Relire le total ne crée aucune inscription.
Les réponses 4xx ne déclenchent aucune nouvelle tentative, y compris la réponse 429 de limitation de débit. Répéter immédiatement une requête refusée pour saturation ajouterait de la charge.
Pour le POST, seule une ConnectionException autorise une nouvelle tentative. Dès qu'une réponse HTTP est reçue, même une erreur serveur, le client la retourne à l'Action sans rejouer l'appel.
L'absence de réponse laisse pourtant le résultat incertain. Un timeout peut survenir après l'enregistrement de l'adresse, avant que la réponse arrive à la vitrine.
Le cloud impose l'unicité de l'email en base et coordonne les soumissions concurrentes par transaction et verrouillage. Si deux créations se heurtent à cette contrainte d'unicité, le traitement relit l'inscription existante.
Ces protections évitent une seconde ligne pour la même adresse. Les autres étapes du traitement peuvent s'exécuter plus d'une fois : une nouvelle tentative du job d'envoi peut renvoyer l'email.
Le lien de confirmation reçu par le web
Le cloud crée le token et envoie un lien vers une page d'aubia.dev. Ce lien comprend le token dans sa query string : le serveur web le reçoit donc au chargement initial.
Une fois React actif, la page lit ce token dans le navigateur et appelle directement le cloud pour confirmer l'inscription. La désinscription suit le même principe. Ces appels ne passent pas par le proxy utilisé pour soumettre l'adresse email.
Le passage initial laisse aussi une trace technique côté web. Laravel conserve l'URL complète comme URL précédente en session, et Inertia l'inclut dans les données de la page.
Le lien permet de confirmer une inscription en attente pendant 48 heures. Le cloud prévoit une purge quotidienne des adresses encore non confirmées après 30 jours, selon la politique de confidentialité. Il peut renvoyer un email de confirmation après une nouvelle soumission ; les emails de bienvenue et de lancement attendent la confirmation.
Le parcours de préinscription détaille le formulaire, la confirmation et le compteur.
Les sessions et préférences du visiteur
Le rate limiter compte les requêtes selon l'adresse IP et une empreinte de l'email, avec un plafond commun. Les clés stockées sont hachées et expirent avec leur fenêtre de limitation. Ces empreintes ne suffisent pas à rendre les données anonymes.
Le contenu éditorial d'une page localisée est commun aux visiteurs. Sa réponse complète varie avec les messages flash, les erreurs de formulaire et les données propres à la requête.
La racine / choisit la langue d'après un cookie de préférence, puis l'en-tête Accept-Language. Les cookies de session et de protection CSRF accompagnent ce cookie de langue. La préférence de thème est conservée séparément dans le localStorage, sans envoi automatique au serveur.
Les données de diagnostic demandent aussi une durée de conservation maîtrisée. Les sauvegardes et la purge de la liste d'inscriptions sont gérées côté cloud.
Une inscription dépendante du réseau
Si le cloud ne répond pas, la vitrine affiche un échec. Elle ne conserve aucune inscription locale à transmettre plus tard. Après un timeout, l'adresse peut malgré tout avoir été enregistrée côté cloud ; une nouvelle soumission suit alors le traitement des doublons.
Le compteur dispose d'un repli différent. La vitrine conserve une dernière valeur connue pendant une durée limitée et peut l'afficher en cas de panne. Sans valeur disponible, le nombre est omis.
Je dois renouveler la clé partagée des deux côtés et maintenir l'API disponible. Une application qui doit accepter des inscriptions hors ligne demanderait un autre fonctionnement. Sur aubia.dev, leur enregistrement, leur confirmation et leur purge sont gérés par aubia.cloud.
La bêta d'Aubia est gratuite et sans limite de temps. Inscrivez-vous pour recevoir votre invitation.
Rejoindre la liste d'attente