El bug invisible que vació mis landings (y cómo lo encontré)

Hoy tenía una tarea aparentemente simple: verificar que los tres landings nuevos de la corporación — Ledger, PAIP y Pólizas — funcionaran correctamente en producción. Los había desplegado ayer con el quiz de perfilamiento activo y las variables de entorno configuradas en Vercel. Todo debería estar en orden.

No estaba en orden.

El síntoma

Al ingresar un correo en el quiz de Ledger, el frontend respondía con un error claro: “Waitlist backend not configured (missing NOTION_TOKEN or NOTION_DATABASE_WAITLIST)”. El mismo error en PAIP. En Pólizas. Los tres landings, con el mismo fallo en el mismo punto exacto del flujo.

Lo extraño: las variables estaban ahí. Podía verlas en Vercel — NOTION_TOKEN y NOTION_DATABASE_WAITLIST, encriptadas, configuradas para producción y desarrollo. Y LabelLoop, el cuarto landing, funcionaba perfecto con exactamente el mismo código.

La investigación

Mi primer instinto fue el clásico “algo falló en el deploy”. Revisé los logs. El deploy era exitoso, 7 segundos, sin errores. Revisé la configuración de Vercel. Todo correcto. Revisé la ruta API en el código fuente — la lógica era impecable.

Entonces hice lo que debería haber hecho primero: leer el archivo compilado.

En Astro con modo servidor (SSR), el build genera archivos .mjs que se despliegan como funciones serverless en Vercel. Busqué el chunk compilado de mi endpoint waitlist.ts en .vercel/output/functions/. Lo que encontré me detuvo:

async function saveToNotion(submission, env) {
  {
    return {
      ok: false,
      status: 503,
      error: "Waitlist backend not configured (missing NOTION_TOKEN or NOTION_DATABASE_WAITLIST)"
    };
  }
}

El cuerpo entero de la función había desaparecido. Solo quedaba el error 503.

La causa raíz

El código fuente usaba import.meta.env.NOTION_TOKEN para leer la variable de entorno. En Astro, import.meta.env es la forma recomendada de acceder a variables de entorno — funciona perfectamente en desarrollo local si tienes un .env con los valores.

Pero Vite, el bundler que usa Astro internamente, evalúa import.meta.env en build time, no en runtime. Cuando construí el proyecto localmente sin esas variables en mi .env local, Vite las leyó como undefined. La condición if (!token || !databaseId) era siempre verdadera. El optimizador aplicó dead-code elimination — eliminó todo el código que nunca podría ejecutarse — y dejó solo el bloque del error.

Luego deploye ese binario muerto a Vercel con --prebuilt. Las variables estaban en Vercel, sí. Pero el código compilado ya no las buscaba.

¿Por qué LabelLoop funcionaba? Porque fue desplegado meses antes, vía el proceso de build normal de Vercel — donde los servidores de Vercel corren el build con las variables ya inyectadas. El código compilado de LabelLoop sí tenía el cuerpo completo de la función.

El fix (y por qué es permanente)

La solución fue cambiar import.meta.env.NOTION_TOKEN por process.env.NOTION_TOKEN en los tres endpoints. process.env es una referencia de runtime pura — Vite nunca la inline, nunca la elimina, siempre la deja como está para que Node.js la resuelva cuando la función se ejecuta en producción.

Un cambio de 13 caracteres. Rebuild, redeploy. Los tres landings respondiendo { ok: true }.

Para que este problema no vuelva a aparecer con un quinto o sexto landing, The Architect implementó hoy una capa de protección: un script de provisión de variables (infra/scripts/provision-landing-env.sh), un job de auditoría en el CI que verifica que ningún proyecto nuevo se despiegue sin las variables necesarias, y un registro canónico de todos los proyectos activos con sus requerimientos documentados.

Lo que también avanzó hoy

Además del debugging, esta sesión tuvo otras piezas en movimiento.

Preparé los materiales completos para la Clase 2 de mi curso en UBO (11 de mayo): un outline de 23 slides sobre vinculación universidad-empresa, un handout con 8 fondos internacionales accesibles desde Chile, y una hoja de ejercicio práctico para los cuatro estudiantes. La clase tiene que estar lista el domingo 10 en la noche — está en el calendario.

También tomé una decisión estratégica sobre Taste Engine, un proyecto nuevo de análisis musical con IA que estaba en evaluación. Wags lo evaluó y la respuesta fue clara: defer. No porque la idea sea mala — el brief está completo y el fit estratégico existe. Sino porque hay deuda técnica activa que bloquea revenue real: PAIP necesita completar su fase de testing, LabelLoop tiene tres problemas de seguridad sin resolver. El potencial de Taste Engine no se evapora en tres semanas. La fecha de activación: 26 de mayo, post docencia.

Finalmente, automaticé parte del flujo de publicación GTM: un agente remoto que corre todos los días a las 13:00, consulta el calendario de contenido en Notion, y crea eventos en Google Calendar con el contenido listo para publicar y las instrucciones de cómo hacerlo. El objetivo es que publicar en redes cueste menos de cinco minutos.

El aprendizaje del día

Cuando algo falla en producción y el código fuente se ve correcto, el siguiente paso es leer el compilado. El optimizador sabe cosas que tú no ves. En este caso, sabía que mi función siempre iba a fallar — porque en el momento del build, así era.

import.meta.env para variables secretas en SSR: nunca más.