Ir al contenido principal

Devlog · Fase 0 · 3/7

Un sitio web sin base de datos

Cero tabla, cero dato personal en reposo: la lista de espera de aubia.dev sale hacia aubia.cloud por un proxy firmado con HMAC, con un reintento que nunca duplica un registro.

Publicado el 27 julio 20266 min de lectura
  • architecture
  • security
  • waitlist
  • gdpr

La landing de Aubia recoge direcciones de correo. Se podría creer que por tanto necesita una base de datos. No tiene ninguna. Ni una tabla, ni un modelo Eloquent, ni una sola dirección de correo escrita en disco del lado del sitio web. No padecí esa ausencia, la puse desde el primer commit. Una página que inscribe a la gente en una lista de espera puede no persistir absolutamente nada, y así es más simple.

Un sitio web no necesita base de datos

Todo el contenido de aubia.dev es estático por naturaleza: los textos de marketing viven en archivos de traducción JSON, los artículos de este blog en archivos Markdown versionados junto al código. Nada de eso justifica una base de datos. El único flujo dinámico de la Fase 0, el registro en la lista de espera, no almacena nada localmente: transita hacia aubia.cloud.

Por eso el repositorio web no lleva ningún modelo Eloquent, ninguna migración de datos de negocio. Lo que toca Redis se limita a la caché transitoria y a la sesión: un contador, un token antiabuso, nunca una dirección de correo duradera.

Quitar la base de datos de un sitio web elimina de golpe toda una clase de preocupaciones: copias de seguridad, cifrado en reposo, filtración de datos, purga RGPD. No se asegura lo que no se guarda.

La frontera entre aubia.dev, sitio web público sin estado, y api.aubia.cloud, que guarda el almacenamiento de la lista de espera, los tokens y el rastro del consentimiento; solo un POST firmado con HMAC la cruza.

Cero dato personal almacenado del lado web

La regla es absoluta: todo dato personal pasa por el cloud, nunca por un almacenamiento local del lado del sitio web. Cuando usted envía su correo, el controller queda deliberadamente delgado. Valida, construye un objeto de transporte tipado, delega en una Action y devuelve un mensaje flash. No conoce ni la red, ni la firma, ni la base de datos, porque no la hay.

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

El correo cruza la memoria del worker durante una única llamada HTTP saliente, y luego desaparece. No queda nada atrás.

Un proxy firmado hacia aubia.cloud

El módulo Waitlist se sumó al repositorio a principios de mayo de 2026, una vez montada la escena de la landing. Su Action SubmitWaitlistEmail hace de puente. Serializa la payload, calcula una firma HMAC SHA-256 y hace un post a aubia.cloud. La firma le prueba al cloud que la petición viene realmente del sitio web oficial, y el origen queda fijado en el contrato.

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

Mantuve deliberadamente esta firma al mínimo: cubre el cuerpo JSON en bruto, sin timestamp ni nonce antirreplay. El sitio web es el único llamador legítimo y su origen está restringido por IP del lado del cloud, lo que hace poco realista un ataque de replay en la Fase 0. Añadir un mecanismo antirreplay habría lastrado el contrato sin beneficio tangible en esta etapa.

En producción, esta firma no se puede desactivar: el código lanza una excepción si alguien intenta apagarla. Un detalle importa en la payload enviada. El formulario atrapa a los robots con un honeypot temporal, un campo started_at fijado al montar la página. Este campo sirve únicamente para la validación local y nunca se transmite al cloud, lo que mantiene mínima la superficie del contrato y estabiliza la firma.

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
}

La firma no está ahí para reemplazar las trampas antibot sino para añadir una capa por encima. Un honeypot o un retardo mínimo puede sortearlos un robot algo cuidadoso, capaz de leer el DOM o de programar una espera. El HMAC, en cambio, encarece el scraping masivo: sin el secreto compartido, no se acepta ninguna petición forjada.

Un reintento que nunca duplica un registro

La red falla a veces. Un cliente HTTP ingenuo reintentaría entonces la petición, con el riesgo de inscribir dos veces a la misma persona. Por eso el cliente saliente distingue dos casos según la idempotencia del verbo. Un GET, como leer el contador, puede reintentarse sin efecto secundario. Un POST de registro no: solo se reintenta si la petición nunca llegó al 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,
)

Si el cloud respondió, aunque sea con un error de servidor, el sitio web no reintenta: mapea la respuesta a un estado de negocio y muestra el mensaje correcto. El arreglo vive en la semántica del reintento, no en las conjeturas.

El double opt-in vive del lado cloud

Es aubia.cloud quien gestiona el double opt-in: generación del token de confirmación, envío del correo, expiración tras 48 horas y, sobre todo, el rastro del consentimiento. Mientras un registro no se confirma, no se envía ninguna comunicación, y las direcciones no confirmadas se purgan automáticamente.

El sitio web nunca ve nada de esto. No conoce ni el token, ni el estado de confirmación, ni el más mínimo dato duradero. Esta frontera nítida entre un front público sin estado y un cloud que guarda la lógica y los datos es lo que hace que el conjunto sea simple de razonar y de asegurar.

Lo que la ausencia de base de datos simplifica

Sin base de datos local, desplegar aubia.dev se vuelve trivial: ninguna migración que ejecutar, ninguna conexión que provisionar, ningún estado que replicar entre workers. La seguridad se reduce en consecuencia, porque el sitio web no guarda ningún secreto de usuario que proteger. Y la conformidad RGPD se traslada a un único sitio, el cloud, en lugar de estar dispersa por cada superficie.

Un sitio web debe informar y convertir, no convertirse en una caja fuerte. Al quitarle su base de datos, le quito sus responsabilidades más pesadas.

Este proxy firmado es solo un eslabón. El recorrido completo, en cambio, merece su propio relato: el formulario, el antibot sin captcha, el double opt-in y el contador, con los compromisos detrás de cada una de estas decisiones.

Para seguir la construcción de Aubia y recibir aviso cuando se abra la beta 0.1, únase a la lista de espera.

Esta obra se cuenta aquí, artículo a artículo. Súbase a bordo: sus comentarios dibujarán lo que viene.

Únase a la lista de espera