Ir al contenido principal

Devlog · Fase 0 · 4/7

El registro previo de punta a punta

Formulario optimista React 19, antibot sin captcha, cuatro estados para un POST y un contador que solo cuenta a los confirmados: el recorrido de registro previo de Aubia, decisión a decisión.

Publicado el 3 agosto 20266 min de lectura
  • waitlist
  • react
  • security
  • gdpr

El artículo anterior mostraba que aubia.dev no tiene ninguna base de datos, y que el registro sale hacia aubia.cloud por un proxy firmado. Aquí subo un peldaño para contar el recorrido completo, desde el campo de correo hasta el correo de confirmación, y sobre todo los compromisos detrás de cada decisión. Un registro previo parece trivial. Hice de él la pieza más cuidada del sitio web.

El flujo animado del double opt-in: POST del visitante a aubia.dev, transmisión firmada con HMAC a api.aubia.cloud, correo con token de un solo uso, clic de confirmación en 48 horas.

Un formulario optimista

El formulario es un componente React 19 conectado a Inertia. Nada recarga: al enviar, una señal optimista parte hacia un toast global, construido con el hook useOptimistic de React 19. La interfaz confirma de inmediato, y luego la respuesta real del servidor reconcilia el estado, sin hacer nada en caso de éxito y anulando la vista en caso de error.

Un detalle cuida la entrada: en el momento en que usted sale del campo, una rutina de sugerencia detecta las erratas comunes en el dominio y ofrece una corrección clicable, del tipo gmial.com corregido en gmail.com. Nada de esto es cosmético. Cada correo mal escrito es un registro perdido que nunca recibirá su confirmación.

La accesibilidad sigue la misma exigencia: etiqueta asociada, aria-invalid en el campo con error, role="alert" en los mensajes, y una sugerencia vinculada por aria-describedby.

Antibot sin fricción: honeypot antes que captcha

Nada de captcha, para empezar. Un captcha protege de los robots gravando a cada humano con una tarea, y mete a un tercero en el recorrido. Preferí dos trampas invisibles. La primera es un campo señuelo, oculto por CSS, que solo un robot rellena y que el servidor rechaza si no está vacío. La segunda es una trampa temporal: una marca de tiempo fijada al montar la página, comparada con el instante del envío. Un robot que envía justo después de la carga se delata solo.

public function rules(): array
{
    return [
        'email' => ['required', 'email:rfc', 'max:254'],
        '_fax' => ['prohibited'],              // campo señuelo, debe quedar vacío
        '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', 'Envío demasiado rápido.');
        }
    });
}

Un throttle con nombre completa el dispositivo del lado servidor, con tres límites simultáneos: uno por dirección IP, otro más estricto por dirección de correo, y un techo global que absorbe un ataque distribuido. Usted no ve nada de esto. Un robot, en cambio, choca con los tres de golpe.

Cuatro estados para un solo POST

Una misma acción, registrarse, cubre varias situaciones reales. El cloud las distingue y el sitio web las traduce en mensajes precisos, en lugar de un vago «está enviado». El proxy lee el estado devuelto por aubia.cloud y lo mapea a un enum de negocio.

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,
};

Cuatro desenlaces de éxito: un registro nuevo, un reenvío de confirmación tras un retardo, un registro ya pendiente, un registro ya confirmado. Cada uno merece su mensaje, porque un visitante que ya confirmó y vuelve a registrarse no debe creer que acaba de empezar de cero.

El double opt-in, o por qué prefiero una lista más pequeña

Llega la decisión que estructura todo lo demás. Podría haber registrado cada correo y dar el registro por hecho. Elegí el double opt-in: el cloud envía un correo con un enlace de confirmación de un solo uso, válido cuarenta y ocho horas, y mientras no se pulse ese enlace, no sale ninguna comunicación. Los registros no confirmados se purgan tras treinta días.

Esta elección encoge deliberadamente la lista. Ese es el objetivo. Una lista de mil direcciones realmente confirmadas vale más que una de cinco mil de las que no sé nada. Prueba un consentimiento con fecha, valida que la dirección existe, y garantiza que en el lanzamiento de la beta 0.1 me dirijo a gente que de verdad levantó la mano. La calidad de la lista prima sobre su volumen mostrado.

Una confirmación con varios desenlaces, fuera del índice

El clic en el enlace aterriza en una página dedicada de aubia.dev. Lee el token en la URL, consulta al cloud y muestra uno de cinco desenlaces: confirmado, ya confirmado, enlace inválido, no encontrado o expirado. Esta página, como la de baja, está explícitamente retirada del índice de los buscadores.

<SeoHead
    path="/waitlist/confirmed"
    robots="noindex"
    title={t('waitlist.confirmed.seo_title')}
/>

Estas URL llevan un token de un solo uso y no tienen ningún valor en búsqueda. Excluirlas del índice evita que un enlace de confirmación quede rondando en los resultados y protege la limpieza del posicionamiento. La baja, por su parte, se hace en un clic desde cada correo, conforme a la norma RFC 8058, y su lógica vive del lado cloud, nunca en el sitio web.

La seguridad de esta página descansa en la semántica del propio token: de un solo uso, hasheado del lado cloud, borrado tras el consumo, expirado a las cuarenta y ocho horas.

Un contador que solo cuenta a los confirmados

Queda la prueba social. La landing muestra un número de registros, pero solo cuenta a las personas que confirmaron, nunca las direcciones pendientes. Y no se muestra hasta que se alcanza un umbral mínimo: por debajo, el componente no renderiza nada. Mejor no mostrar cifra que mostrar una baja y desalentadora al inicio de la campaña. Un contador honesto no puede jugar a dos bandas. O infla el número con registros no confirmados y miente, o solo muestra lo real.

Toda la lógica de registro previo se inclina en el mismo sentido: lo real, medido con sobriedad, y nada que no sea demostrable. Del lado RGPD, se resume en una frase bajo el formulario, una finalidad única, la invitación a la beta, y ningún dato personal en reposo en el sitio web.

Todo esto por un campo de correo

Este recorrido moviliza varios bloques: un formulario, una validación, un enum de estados, un proxy, un contador. En un repositorio tan pequeño, todo eso merece ordenarse con método en lugar de dispersarse. Esa organización en módulos, se la abro justo después.

Para 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