Skip to main content

Devlog · Phase 0 · 3/7

A Marketing Site With No Database

Zero tables, zero personal data at rest: aubia.dev's waitlist goes to aubia.cloud through an HMAC-signed proxy, with a retry that never doubles a signup.

Published on 27 July 20266 min read
  • architecture
  • security
  • waitlist
  • gdpr

Aubia's landing page collects email addresses. You'd think it therefore needs a database. It has none. No table, no Eloquent model, not a single email address written to disk on the website side. I didn't inherit this absence, I set it from the first commit. A page that signs people up to a waitlist can persist nothing at all, and it's simpler that way.

A Marketing Site Doesn't Need a Database

All of aubia.dev's content is static by nature: the marketing copy lives in JSON translation files, this blog's articles in Markdown files versioned alongside the code. None of that justifies a database. Phase 0's only dynamic flow, the waitlist signup, stores nothing locally: it passes through to aubia.cloud.

So the web repo carries no Eloquent model, no business data migration. What touches Redis is limited to transient cache and the session: a counter, an anti-abuse token, never a durable email address.

Removing the database from a marketing site removes an entire class of concerns at once: backups, encryption at rest, data breaches, GDPR purges. You don't secure what you don't hold.

The boundary between aubia.dev, the stateless public site, and api.aubia.cloud holding waitlist storage, tokens and the consent trail; only an HMAC-signed POST crosses it.

Zero Personal Data Stored on the Web Side

The rule is absolute: all personal data goes through the cloud, never through local storage on the website side. When you submit your email, the controller stays deliberately thin. It validates, builds a typed transport object, delegates to an Action, and returns a flash message. It knows nothing of the network, the signature, or the database, because there is none.

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

The email crosses the worker's memory for the duration of one outbound HTTP call, then vanishes. Nothing is left behind.

A Signed Proxy to aubia.cloud

The Waitlist module joined the repo in early May 2026, once the landing page's scene was set. Its SubmitWaitlistEmail Action does the bridging. It serializes the payload, computes an HMAC SHA-256 signature, and posts to aubia.cloud. The signature proves to the cloud that the request really comes from the official website, and the origin is pinned in the contract.

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

I deliberately kept this signature minimal: it covers the raw JSON body, with no timestamp or anti-replay nonce. The website is the only legitimate caller and its origin is IP-restricted on the cloud side, which makes a replay attack unrealistic in Phase 0. Adding an anti-replay mechanism would have weighed the contract down with no tangible benefit at this stage.

In production, this signature can't be disabled: the code throws an exception if someone tries to turn it off. One detail matters in the payload sent. The form traps bots with a time-based honeypot, a started_at field set when the page mounts. This field is used only for local validation and is never transmitted to the cloud, which keeps the contract's surface minimal and stabilizes the signature.

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
}

The signature isn't there to replace the anti-bot traps but to add a layer on top. A honeypot or a minimum delay can be worked around by a somewhat careful bot, one able to read the DOM or schedule a wait. The HMAC, on the other hand, makes mass scraping expensive: without the shared secret, no forged request is accepted.

A Retry That Never Doubles a Signup

The network fails sometimes. A naive HTTP client would then replay the request, at the risk of signing the same person up twice. So the outbound client distinguishes two cases based on the verb's idempotency. A GET, like reading the counter, can be replayed with no side effect. A signup POST cannot: it only replays if the request never reached the 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,
)

If the cloud responded, even with a server error, the website doesn't retry: it maps the response to a business status and shows the right message. The fix lives in the retry's semantics, not in guesswork.

The Double Opt-In Lives on the Cloud Side

It's aubia.cloud that handles the double opt-in: generating the confirmation token, sending the email, expiring after 48 hours, and above all recording consent. As long as a signup isn't confirmed, no communication goes out, and unconfirmed addresses are purged automatically.

The website never sees any of this. It knows nothing of the token, the confirmation state, or any durable data. This clean boundary between a stateless public front end and a cloud that holds the logic and the data is what makes the whole thing simple to reason about and to secure.

What the Absence of a Database Simplifies

Without a local database, deploying aubia.dev becomes trivial: no migration to run, no connection to provision, no state to replicate across workers. Security shrinks accordingly, because the website holds no user secret to protect. And GDPR compliance moves to a single place, the cloud, instead of being scattered across every surface.

A marketing site should inform and convert, not become a vault. By taking its database away, I'm taking away its heaviest responsibilities.

This signed proxy is only one link. The full journey, though, deserves its own telling: the form, the captcha-free anti-bot, the double opt-in, and the counter, with the trade-offs behind each of these choices.

To follow Aubia's construction and be notified when the beta 0.1 opens, join the waitlist.

This build is chronicled here, article after article. Come aboard: your feedback will shape what comes next.

Join the waitlist