Devlog · Phase 0 · 4/7
Die Voranmeldung von Anfang bis Ende
Optimistisches React-19-Formular, Anti-Bot ohne Captcha, vier Status für einen POST und ein Zähler, der nur die Bestätigten zählt: der Voranmeldungsweg von Aubia, Abwägung für Abwägung.
- waitlist
- react
- security
- gdpr

Der vorige Artikel zeigte, dass aubia.dev keine Datenbank hat und dass die Anmeldung über einen signierten Proxy an aubia.cloud geht. Hier zoome ich eine Stufe heraus, um den ganzen Weg zu erzählen, vom E-Mail-Feld bis zur Bestätigungs-E-Mail, und vor allem die Abwägungen hinter jeder Entscheidung. Eine Voranmeldung wirkt trivial. Ich habe daraus das sorgfältigste Stück der Website gemacht.
Ein optimistisches Formular
Das Formular ist eine React-19-Komponente, angebunden an Inertia. Nichts lädt neu: Beim Absenden geht ein optimistisches Signal an einen globalen Toast, gebaut mit dem useOptimistic-Hook von React 19. Die Oberfläche bestätigt sofort, dann versöhnt die echte Server-Antwort den Zustand, ohne bei Erfolg etwas zu tun und indem sie die Anzeige bei einem Fehler zurücknimmt.
Ein Detail pflegt die Eingabe: In dem Moment, in dem Sie das Feld verlassen, erkennt eine Vorschlagsroutine die häufigen Tippfehler in der Domain und schlägt eine anklickbare Korrektur vor, etwa gmial.com zu gmail.com korrigiert. Nichts davon ist kosmetisch. Jede falsch getippte E-Mail ist ein verlorener Interessent, der seine Bestätigung nie erhalten wird.
Die Barrierefreiheit folgt derselben Anforderung: zugeordnetes Label, aria-invalid auf dem fehlerhaften Feld, role="alert" auf den Meldungen und ein über aria-describedby verknüpfter Vorschlag.
Anti-Bot ohne Reibung: Honeypot statt Captcha
Kein Captcha, um damit anzufangen. Ein Captcha schützt vor Bots, indem es jedem Menschen eine Last aufbürdet, und es holt einen Dritten in den Weg. Ich habe zwei unsichtbare Fallen vorgezogen. Die erste ist ein Köderfeld, per CSS versteckt, das nur ein Bot ausfüllt und das der Server ablehnt, wenn es nicht leer ist. Die zweite ist eine zeitliche Falle: ein beim Mounten der Seite gesetzter Zeitstempel, verglichen mit dem Moment des Absendens. Ein Bot, der unmittelbar nach dem Laden absendet, verrät sich von selbst.
public function rules(): array
{
return [
'email' => ['required', 'email:rfc', 'max:254'],
'_fax' => ['prohibited'], // Köderfeld, muss leer bleiben
'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', 'Zu schnell abgesendet.');
}
});
}
Ein benannter Throttle ergänzt das Ganze auf der Server-Seite, mit drei gleichzeitigen Grenzen: eine pro IP-Adresse, eine strengere pro E-Mail-Adresse und eine globale Obergrenze, die einen verteilten Angriff abfedert. Sie sehen davon nichts. Ein Bot dagegen stößt an alle drei auf einmal.
Vier Status für einen einzigen POST
Ein und dieselbe Handlung, sich anzumelden, deckt mehrere reale Situationen ab. Die Cloud unterscheidet sie, und die Website übersetzt sie in präzise Meldungen statt in ein vages "Es ist abgeschickt". Der Proxy liest den von aubia.cloud zurückgegebenen Status und bildet ihn auf ein Geschäfts-Enum ab.
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,
};
Vier Erfolgsausgänge: eine neue Anmeldung, ein erneuter Bestätigungsversand nach einer Frist, eine bereits wartende Anmeldung, eine bereits bestätigte Anmeldung. Jede verdient ihre Meldung, denn ein Besucher, der bereits bestätigt hat und sich erneut anmeldet, darf nicht glauben, er habe gerade wieder bei null angefangen.
Der Double-Opt-in, oder warum ich eine kleinere Liste bevorzuge
Nun kommt die Entscheidung, die alles Übrige strukturiert. Ich hätte jede E-Mail speichern und die Anmeldung als erledigt betrachten können. Ich habe den Double-Opt-in gewählt: Die Cloud sendet eine E-Mail mit einem einmaligen Bestätigungslink, achtundvierzig Stunden gültig, und solange dieser Link nicht angeklickt wird, geht keine Kommunikation hinaus. Unbestätigte Anmeldungen werden nach dreißig Tagen gelöscht.
Diese Entscheidung verkleinert die Liste absichtlich. Das ist der Zweck. Eine Liste von tausend wirklich bestätigten Adressen ist mehr wert als eine Liste von fünftausend, über die ich nichts weiß. Sie belegt eine datierte Einwilligung, sie validiert, dass die Adresse existiert, und sie garantiert, dass ich mich beim Start der Beta 0.1 an Menschen wende, die wirklich die Hand gehoben haben. Die Qualität der Liste geht vor ihrem angezeigten Umfang.
Eine Bestätigung mit mehreren Ausgängen, außerhalb des Index
Der Klick auf den Link landet auf einer eigenen Seite von aubia.dev. Sie liest das Token in der URL, fragt die Cloud ab und zeigt einen von fünf Ausgängen: bestätigt, bereits bestätigt, ungültiger Link, nicht gefunden oder abgelaufen. Diese Seite ist, wie die der Abmeldung, ausdrücklich aus dem Index der Suchmaschinen entfernt.
<SeoHead
path="/waitlist/confirmed"
robots="noindex"
title={t('waitlist.confirmed.seo_title')}
/>
Diese URLs tragen ein Einmal-Token und haben keinerlei Wert in der Suche. Sie aus dem Index auszuschließen, verhindert, dass ein Bestätigungslink in Ergebnissen herumliegt, und schützt die Sauberkeit des Rankings. Die Abmeldung wiederum erfolgt mit einem Klick aus jeder E-Mail, gemäß der Norm RFC 8058, und ihre Logik lebt auf der Cloud-Seite, niemals auf der Website.
Die Sicherheit dieser Seite hängt an der Semantik des Tokens selbst: einmalig, auf der Cloud-Seite gehasht, nach Verbrauch gelöscht, nach achtundvierzig Stunden abgelaufen.
Ein Zähler, der nur die Bestätigten zählt
Bleibt der Social Proof. Die Landingpage zeigt eine Anzahl Angemeldeter, aber sie zählt nur die Personen, die bestätigt haben, niemals die wartenden Adressen. Und sie erscheint nicht, solange eine Mindestschwelle nicht erreicht ist: darunter rendert die Komponente nichts. Es ist besser, keine Zahl anzuzeigen, als eine niedrige und entmutigende zu Beginn der Kampagne. Ein ehrlicher Zähler kann nicht auf beiden Seiten spielen. Entweder bläht er die Zahl mit unbestätigten Anmeldungen auf und lügt, oder er zeigt nur das Reale.
Die gesamte Voranmeldungs-Logik neigt zur selben Seite: dem Realen, nüchtern gemessen, und nichts, was sich nicht belegen ließe. In Sachen DSGVO läuft das auf einen Satz unter dem Formular hinaus, einen einzigen Zweck, die Einladung zur Beta, und keine personenbezogenen Daten im Ruhezustand auf der Website.
All das für ein E-Mail-Feld
Dieser Weg mobilisiert mehrere Bausteine: ein Formular, eine Validierung, ein Status-Enum, einen Proxy, einen Zähler. In einem so kleinen Repo verdient all das, mit Methode geordnet statt verstreut zu werden. Diese Organisation in Module öffne ich Ihnen gleich danach.
Um bei der Öffnung der Beta 0.1 benachrichtigt zu werden, tragen Sie sich in die Warteliste ein.
Dieses Projekt wird hier erzählt, Artikel für Artikel. Kommen Sie an Bord: Ihr Feedback zeichnet den weiteren Weg.
Auf die Warteliste