Devlog · Fase 0 · 3/7
Recopilar direcciones de correo sin una tabla local
Las inscripciones se almacenan en aubia.cloud. El sitio conserva sesiones y una caché Redis, con solicitudes firmadas y un formulario que depende de la red.
por Mike, fundador de Aubia Publicado el Actualizado el 8 min de lectura
- architecture
- security
- waitlist
- gdpr



aubia.dev recibe inscripciones en la lista de espera sin almacenarlas en una tabla local. El repositorio del sitio público no contiene ni un modelo Eloquent ni una migración.
Confié a aubia.cloud el almacenamiento de las inscripciones. El sitio público valida el formulario y después reenvía la dirección de correo por HTTP. El cloud almacena la inscripción, envía el enlace de confirmación y procesa el clic que recibe.
Esta separación me evita mantener una segunda base de datos de inscripciones. Hace que el formulario dependa de la red, pero no me exime de proteger los datos que gestiona el sitio. Las sesiones y la caché de Redis forman parte de esa responsabilidad.
Contenido versionado con el código
Los textos del sitio público se almacenan en archivos JSON y sus artículos en archivos Markdown. El repositorio contiene los seis idiomas que sirve el sitio. Una corrección de texto se publica en el mismo despliegue que una corrección de código.
En producción, el blog lee un catálogo local compilado durante el build. Contiene títulos, fechas y el HTML generado por Markdown y Phiki, incluidos los artículos programados. Un catálogo ausente, inválido o desactualizado produce una respuesta 503 en las rutas que dependen de él; no se ejecuta un renderizado Phiki alternativo durante la petición. El parsing con caché Redis sigue disponible en local y en los tests, con entradas que caducan al cabo de 24 horas.
La fecha de publicación se comprueba después de leer el catálogo, en cada solicitud. Un artículo programado ya compilado queda accesible en la fecha prevista sin otro despliegue. La presencia de su HTML en el catálogo no lo hace público antes de esa fecha.
Redis también almacena las sesiones y los contadores que utiliza el rate limiter, que limita la frecuencia de las solicitudes. La base de datos de inscripciones está alojada en aubia.cloud.
Los campos enviados al cloud
El FormRequest valida los campos y construye un DTO, un objeto tipado que transporta los datos. El controller transmite este objeto a la Action SubmitWaitlistEmailAction.
El cuerpo saliente contiene la dirección de correo, el idioma, los parámetros UTM de la campaña original y el país del visitante, leído en la cabecera que Cloudflare añade a su petición. La marca de tiempo de la comprobación del retraso mínimo sirve para la validación local y no se incluye en el DTO.
El estado que devuelve la Action determina el mensaje que se muestra después de la redirección:
// Fragmento de WaitlistController.
$status = $action->execute($request->toData());
if ($status->isSuccess()) {
return back()->with('waitlist_success', $status->messageKey());
}
return back()->with('waitlist_error', $status->messageKey());
El mensaje flash se conserva en la sesión para la siguiente solicitud. Contiene una clave de traducción para el resultado, sin copiar la dirección de correo.
Un error de validación exige otra precaución. Laravel normalmente conserva en la sesión los valores anteriores del formulario para volver a mostrarlos. El sitio público excluye explícitamente la dirección de correo de ese mecanismo:
// bootstrap/app.php, configuración de excepciones.
$exceptions->dontFlash(['email']);
La firma del cuerpo exacto
La Action serializa los campos como JSON y después calcula un HMAC SHA-256 mediante una clave compartida. Esta firma permite al cloud verificar el cuerpo recibido con la misma clave.
En este fragmento, los nombres de la clave de configuración y de la cabecera de firma son ilustrativos:
$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),
];
El cliente HTTP envía exactamente el cuerpo que se firmó. La clave no se envía en esta llamada. La cabecera Origin indica el origen esperado por el contrato; el propio remitente declara ese valor.
HTTPS protege el transporte. HMAC verifica la integridad del cuerpo y que el remitente conoce la clave. El formulario de origen de una solicitud firmada puede haber sido rellenado por un programa o por una persona, y la solicitud se puede reproducir más tarde.
El honeypot y el filtro de retraso mínimo descartan algunas solicitudes automatizadas. Un bot aún puede esquivarlos adaptando sus campos o esperando antes de enviar. La firma y estos filtros resuelven problemas distintos.
En producción, el sitio público no envía si la firma está desactivada o falta la clave. El ajuste que permite enviar sin firma en local solo se aplica al sitio público: no desactiva la verificación en el cloud.
La incertidumbre tras un error de red
El CloudApiClient gestiona estas dos llamadas de forma distinta. El GET del contador se puede reintentar tras un error de red o una respuesta 5xx. Volver a leer el total no crea una inscripción.
Las respuestas 4xx no activan ningún reintento, incluida la respuesta 429 de limitación de frecuencia. Repetir de inmediato una solicitud rechazada porque el servicio está saturado añadiría carga.
En el POST, solo una ConnectionException permite reintentar. En cuanto se recibe una respuesta HTTP, incluso un error del servidor, el cliente devuelve la respuesta a la Action sin repetir la llamada.
La ausencia de respuesta sigue dejando incierto el resultado. Puede producirse un timeout después de que la dirección se haya almacenado y antes de que la respuesta llegue al sitio público.
El cloud garantiza la unicidad de las direcciones de correo en la base de datos y coordina las inscripciones simultáneas mediante transacciones y bloqueos. Si dos creaciones chocan con esa restricción de unicidad, el proceso vuelve a leer la inscripción existente.
Estas protecciones impiden una segunda fila para la misma dirección. Los demás pasos del procesamiento pueden ejecutarse más de una vez: un nuevo intento del job de envío puede reenviar el correo.
El enlace de confirmación que recibe el sitio
El cloud crea el token y envía un enlace a una página de aubia.dev. El enlace incluye el token en su query string, por lo que el servidor web lo recibe en la carga inicial.
Cuando React está activo, la página lee el token en el navegador y llama directamente al cloud para confirmar la inscripción. La baja sigue el mismo principio. Estas llamadas no pasan por el proxy utilizado para enviar la dirección de correo.
La visita inicial también deja un rastro técnico en el lado web. Laravel almacena la URL completa como URL anterior en la sesión, e Inertia la incluye en los datos de la página.
El enlace puede confirmar una inscripción pendiente durante 48 horas. El cloud programa una purga diaria de las direcciones que siguen sin confirmar después de 30 días, como se describe en la política de privacidad. Puede volver a enviar un correo de confirmación tras un nuevo envío del formulario. Los correos de bienvenida y de lanzamiento esperan a que se confirme la inscripción.
El flujo de inscripción en la lista de espera cubre con detalle el formulario, la confirmación y el contador.
Sesiones y preferencias de los visitantes
El rate limiter cuenta las solicitudes a partir de la dirección IP y de una huella de la dirección de correo, y aplica además un límite global. Las claves almacenadas son hashes y caducan con la ventana de limitación. Estas huellas no bastan para anonimizar los datos.
El contenido editorial de una página localizada es el mismo para todos los visitantes. La respuesta completa cambia según los mensajes flash, los errores del formulario y los datos específicos de la solicitud.
La ruta raíz / selecciona el idioma a partir de una cookie de preferencia y después de la cabecera Accept-Language. Las cookies de sesión y de protección CSRF acompañan a esta cookie de idioma. La preferencia de tema se almacena por separado en localStorage, sin enviarse automáticamente al servidor.
Los datos de diagnóstico también necesitan un periodo de conservación controlado. Las copias de seguridad y la purga de la lista de espera se gestionan en el cloud.
Una inscripción dependiente de la red
Si el cloud no responde, el sitio público informa de un fallo. No conserva una inscripción local ni la deja en una cola para enviarla más tarde. Tras un timeout, es posible que la dirección ya se haya almacenado en el cloud; un nuevo envío pasa entonces por la lógica de tratamiento de duplicados.
El contador tiene un mecanismo de respaldo distinto. El sitio público conserva el último valor conocido durante un tiempo limitado y puede mostrarlo durante una interrupción. Si no hay ningún valor disponible, omite el recuento.
Tengo que rotar la clave compartida en ambos lados y mantener la API disponible. Una aplicación que deba aceptar inscripciones sin conexión necesitaría otro diseño. En aubia.dev, aubia.cloud gestiona el almacenamiento de las inscripciones, su confirmación y su purga.
La beta de Aubia es gratuita y sin límite de tiempo. Inscríbase para recibir su invitación.
Únase a la lista de espera