Zum Hauptinhalt springen

Devlog · Phase 0 · 3/7

Eine Website ohne Datenbank

Null Tabellen, null personenbezogene Daten im Ruhezustand: Die Warteliste von aubia.dev geht über einen HMAC-signierten Proxy an aubia.cloud, mit einem Retry, der eine Anmeldung nie verdoppelt.

Veröffentlicht am 27 Juli 20265 Min. Lesezeit
  • architecture
  • security
  • waitlist
  • gdpr

Aubias Landingpage sammelt E-Mail-Adressen. Man könnte meinen, sie brauche daher eine Datenbank. Sie hat keine. Keine Tabelle, kein Eloquent-Model, keine einzige E-Mail-Adresse, die auf der Website-Seite auf die Platte geschrieben wird. Ich habe dieses Fehlen nicht hingenommen, ich habe es vom ersten Commit an so angelegt. Eine Seite, die Menschen in eine Warteliste einträgt, kann überhaupt nichts persistieren, und so ist es einfacher.

Eine Website braucht keine Datenbank

Der gesamte Inhalt von aubia.dev ist von Natur aus statisch: Der Marketing-Text lebt in JSON-Übersetzungsdateien, die Artikel dieses Blogs in Markdown-Dateien, die neben dem Code versioniert sind. Nichts davon rechtfertigt eine Datenbank. Der einzige dynamische Flow der Phase 0, die Warteliste-Anmeldung, speichert lokal nichts: Sie wird an aubia.cloud durchgereicht.

Das Web-Repo trägt daher kein Eloquent-Model, keine Migration von Geschäftsdaten. Was Redis berührt, beschränkt sich auf transienten Cache und die Session: einen Zähler, ein Anti-Missbrauchs-Token, niemals eine dauerhafte E-Mail-Adresse.

Die Datenbank aus einer Website zu entfernen, entfernt auf einen Schlag eine ganze Klasse von Sorgen: Backups, Verschlüsselung im Ruhezustand, Datenlecks, DSGVO-Löschungen. Man sichert nicht, was man nicht besitzt.

Die Grenze zwischen aubia.dev, der zustandslosen öffentlichen Website, und api.aubia.cloud mit Wartelisten-Speicher, Tokens und Einwilligungsnachweis; nur ein HMAC-signierter POST überquert sie.

Null personenbezogene Daten auf der Web-Seite gespeichert

Die Regel ist absolut: Alle personenbezogenen Daten laufen über die Cloud, niemals über lokale Speicherung auf der Website-Seite. Wenn Sie Ihre E-Mail absenden, bleibt der Controller bewusst dünn. Er validiert, baut ein typisiertes Transportobjekt, delegiert an eine Action und gibt eine Flash-Meldung zurück. Er weiß nichts vom Netzwerk, von der Signatur oder von der Datenbank, denn es gibt keine.

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

Die E-Mail durchquert den Speicher des Workers für die Dauer eines ausgehenden HTTP-Aufrufs, dann verschwindet sie. Nichts bleibt zurück.

Ein signierter Proxy zu aubia.cloud

Das Waitlist-Modul kam Anfang Mai 2026 ins Repo, sobald die Kulisse der Landingpage stand. Seine Action SubmitWaitlistEmail erledigt die Überbrückung. Sie serialisiert die Payload, berechnet eine HMAC-SHA-256-Signatur und postet an aubia.cloud. Die Signatur beweist der Cloud, dass die Anfrage wirklich von der offiziellen Website kommt, und die Herkunft ist im Vertrag festgelegt.

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

Ich habe diese Signatur bewusst minimal gehalten: Sie deckt den rohen JSON-Body ab, ohne Zeitstempel oder Anti-Replay-Nonce. Die Website ist der einzige legitime Aufrufer, und ihre Herkunft ist auf der Cloud-Seite per IP eingeschränkt, was einen Replay-Angriff in Phase 0 wenig realistisch macht. Einen Anti-Replay-Mechanismus hinzuzufügen, hätte den Vertrag ohne greifbaren Nutzen in diesem Stadium beschwert.

In der Produktion lässt sich diese Signatur nicht abschalten: Der Code wirft eine Exception, wenn jemand versucht, sie zu deaktivieren. Ein Detail zählt in der gesendeten Payload. Das Formular fängt Bots mit einem zeitbasierten Honeypot ab, einem started_at-Feld, das beim Mounten der Seite gesetzt wird. Dieses Feld dient nur der lokalen Validierung und wird niemals an die Cloud übertragen, was die Oberfläche des Vertrags minimal hält und die Signatur stabilisiert.

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
}

Die Signatur ist nicht dazu da, die Anti-Bot-Fallen zu ersetzen, sondern eine Schicht obendrauf zu legen. Ein Honeypot oder eine Mindestverzögerung lässt sich von einem einigermaßen sorgfältigen Bot umgehen, der das DOM lesen oder eine Wartezeit einplanen kann. Das HMAC hingegen macht Massen-Scraping teuer: ohne das gemeinsame Geheimnis wird keine gefälschte Anfrage akzeptiert.

Ein Retry, der nie eine Anmeldung verdoppelt

Das Netzwerk fällt manchmal aus. Ein naiver HTTP-Client würde die Anfrage dann wiederholen, auf die Gefahr hin, dieselbe Person zweimal einzutragen. Deshalb unterscheidet der ausgehende Client zwei Fälle anhand der Idempotenz des Verbs. Ein GET, wie das Lesen des Zählers, kann ohne Nebenwirkung wiederholt werden. Ein Anmelde-POST nicht: Er wird nur wiederholt, wenn die Anfrage die Cloud nie erreicht hat.

->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,
)

Wenn die Cloud geantwortet hat, selbst mit einem Serverfehler, wiederholt die Website nicht: Sie bildet die Antwort auf einen Geschäftsstatus ab und zeigt die richtige Meldung. Die Lösung liegt in der Semantik des Retry, nicht in Mutmaßung.

Der Double-Opt-in lebt auf der Cloud-Seite

Es ist aubia.cloud, das den Double-Opt-in abwickelt: das Bestätigungstoken erzeugen, die E-Mail senden, nach 48 Stunden ablaufen und vor allem die Einwilligung protokollieren. Solange eine Anmeldung nicht bestätigt ist, geht keine Kommunikation hinaus, und unbestätigte Adressen werden automatisch gelöscht.

Die Website sieht davon nie etwas. Sie weiß nichts vom Token, vom Bestätigungszustand oder von irgendwelchen dauerhaften Daten. Diese saubere Grenze zwischen einem zustandslosen öffentlichen Frontend und einer Cloud, die die Logik und die Daten hält, ist es, die das Ganze einfach zu durchdenken und zu sichern macht.

Was das Fehlen einer Datenbank vereinfacht

Ohne lokale Datenbank wird das Deployment von aubia.dev trivial: keine Migration auszuführen, keine Verbindung bereitzustellen, kein Zustand über die Worker zu replizieren. Die Sicherheit schrumpft entsprechend, denn die Website hält kein Nutzergeheimnis, das es zu schützen gälte. Und die DSGVO-Konformität verlagert sich an einen einzigen Ort, die Cloud, statt über jede Oberfläche verstreut zu sein.

Eine Website soll informieren und konvertieren, nicht zum Tresor werden. Indem ich ihr die Datenbank nehme, nehme ich ihr ihre schwersten Verantwortlichkeiten.

Dieser signierte Proxy ist nur ein Glied in der Kette. Der ganze Weg verdient sein eigenes Kapitel: das Formular, das Captcha-freie Anti-Bot-Vorgehen, der Double-Opt-in und der Zähler, mit den Abwägungen hinter jeder dieser Entscheidungen.

Um die Entwicklung von Aubia zu verfolgen und benachrichtigt zu werden, sobald die Beta 0.1 öffnet, 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