El ecosistema JavaScript ha evolucionado hasta ofrecer entornos de ejecución alternativos a Node.js que prometen mayor eficiencia, arranques instantáneos y una integración nativa con la plataforma web. Sin embargo, cuando un equipo de desarrollo decide abandonar Node para adoptar workerd —el runtime open-source que impulsa Cloudflare Workers— se enfrenta a una realidad implacable: los SDK de Node, diseñados para un mundo con sistema de archivos, módulos nativos y estado persistente en memoria, simplemente se rompen. En este artículo compartimos nuestra experiencia real migrando un backend SaaS hacia workerd, los fallos que encontramos, las soluciones que implementamos y las lecciones que todo equipo técnico debería considerar antes de emprender un camino similar.
En Q2BSTUDIO, una empresa de desarrollo de software y tecnología especializada en aplicaciones a medida, estamos acostumbrados a tomar decisiones de arquitectura que maximicen la flexibilidad y minimicen los costes operativos. Nuestro producto principal es un asistente de soporte potenciado por inteligencia artificial, desplegado como backend serverless. Tras meses funcionando sobre Node.js, notamos que los tiempos de arranque en frío, la gestión de contenedores y la dependencia de imágenes dockerizadas empezaban a lastrar tanto el desarrollo como la experiencia de usuario. Decidimos migrar a workerd, seducidos por la promesa de un runtime universal con arranques casi instantáneos y despliegues atómicos. Lo que no anticipamos es que la mayoría de los SDK de terceros que usábamos —Stripe, Resend, Better Auth— se romperían de forma categórica en el nuevo entorno.
El problema fundamental es que workerd implementa el estándar Web Platform: fetch, Request/Response, crypto.subtle, TextEncoder, streams. No ofrece la librería estándar de Node, ni sistema de archivos, ni estado de módulo persistente entre peticiones. Los SDK de Node, incluso aquellos con una API pública limpia, arrastran dependencias profundas que invocan módulos nativos como crypto, fs o http. Cuando el empaquetador de workerd encuentra esas dependencias, la compilación falla. No hay margen para el 'works on my machine': el error ocurre en tiempo de construcción, no en producción. Es brutal, pero brutal en CI, no a las tres de la madrugada.
El primer SDK que sucumbió fue el de Resend, nuestro proveedor de correo transaccional. El paquete oficial de npm parecía inofensivo, pero tiraba de svix, una librería de verificación de webhooks que a su vez necesitaba módulos de Node. Como enviar un email es una única llamada HTTP, lo reemplazamos por una función fetch directa a la API de Resend. El código resultante es mínimo: una petición POST con cabecera Bearer y cuerpo JSON, sin cliente, sin reintentos automáticos, sin árbol de dependencias. Además, implementamos un pequeño validador de direcciones que rechaza el símbolo $ —no por estándar RFC, sino para detectar variables de entorno sin expandir que causarían errores 422. La lección: a veces lo más simple funciona mejor, y eliminar un SDK de terceros puede reducir la superficie de ataque y mejorar la mantenibilidad.
El segundo golpe llegó con el SDK de Stripe para Node, la pieza central de nuestra facturación por uso. Stripe espera cuerpos codificados en formulario con una convención de corchetes para anidar objetos —line_items[0][price]=price_123— que el SDK oculta tras una capa de abstracción. Al no disponer de esa capa, tuvimos que implementar un codificador recursivo manual. Más crítico aún: la verificación de webhooks de Stripe utiliza HMAC-SHA256 y requiere una comparación en tiempo constante para evitar fugas de información. Escribimos nuestra propia función verifyStripeSignature, que extrae el timestamp y la firma de la cabecera stripe-signature, calcula el HMAC con crypto.subtle y compara en tiempo constante, rechazando firmas con más de 300 segundos de antigüedad. Este mismo patrón lo replicamos para los webhooks de Resend, pero con diferencias sutiles: Svix firma ${id}.${timestamp}.${body}, usa clave codificada en base64 y firmas separadas por espacios. Dos esquemas, dos verificadores manuales que ahora mantenemos nosotros. El coste real de abandonar el SDK es que los documentos del proveedor, no su código, se convierten en nuestra especificación.
El tercer SDK que no sobrevivió fue el plugin de passkeys de Better Auth. Nunca llamamos al código ofensivo: el plugin dependía de @simplewebauthn/server, que a su vez dependía de @peculiar/x509 y asn1js —un parser de certificados X.509 necesario para la verificación de atestación WebAuthn. En workerd, esa cadena de dependencias rompió el empaquetado. El resto de Better Auth funciona perfectamente; solo el plugin de passkeys fue eliminado antes del lanzamiento. La moraleja: la dependencia que mata tu despliegue rara vez está en tu package.json; suele ser el parser de certificados que tu plugin de autenticación necesita para un flujo que quizás nunca uses.
Más allá de los SDK rotos, workerd impone cambios sutiles en el modelo de ejecución. Las variables de entorno llegan como bindings en cada petición, no como process.env en ámbito global. Por tanto, construimos el cliente de autenticación en cada solicitud: createAuth(env). El estado en memoria es un espejismo porque workerd distribuye el tráfico entre múltiples isolates efímeros; el limitador de tasa por defecto de Better Auth, que usa memoria local, se vuelve decorativo. Lo reemplazamos por un contador respaldado en el binding de estado KV, que sí persiste entre isolates. También descubrimos que no hay sistema de archivos, así que nuestro widget embebido se sirve como una constante de cadena generada en tiempo de construcción, versionada atómicamente con la API que lo sirve.
La ausencia de atomicidad en KV nos obligó a decidir explícitamente cómo fallar en cada punto. El limitador de tasa en endpoints públicos falla abierto —si el almacén no está disponible, permitimos el paso— porque no queremos que un problema de infraestructura bloquee a todos los clientes. La reserva de idempotencia en la ruta de facturación falla cerrado —si no podemos verificar el estado, bloqueamos la operación— para evitar duplicados con dinero de por medio. Cada decisión de fallo está documentada en el código, y el equipo sabe exactamente qué esperar ante una caída parcial.
No todo fueron roturas. Hono, el framework HTTP que usamos, funciona de forma nativa en workerd. Drizzle se conecta sin problemas a la base de datos SQLite compatible con D1. Zod, el validador de esquemas, es JavaScript puro. El núcleo de Better Auth funciona. El SDK de Vercel AI y nuestro proveedor de gateway de IA también empaquetan sin errores. El ecosistema web estándar es sólido; el problema se concentra en los SDK de proveedores con árboles de dependencias profundos, y no se puede predecir simplemente leyendo las importaciones de tu propio código.
Desde una perspectiva empresarial, la migración a workerd nos ofreció despliegues que no pueden funcionar a medias: si empaqueta, funciona. La superficie de dependencias se redujo drásticamente; la integración con Stripe pasó de un SDK entero a 289 líneas que cualquier desarrollador puede leer en una sentada. Cada llamada HTTP de facturación es visible en un solo archivo. La portabilidad web estándar permite ejecutar el mismo código en Cloudflare Workers, Deno o cualquier runtime que implemente la plataforma web, lo cual encaja con nuestro modelo de autohospedaje. Sin embargo, pagamos un precio: ahora somos los mantenedores de clientes que antes mantenían los proveedores. Cuando Stripe lanza una nueva versión de API, nadie actualiza un paquete por nosotros; leemos el changelog y editamos stripe.ts. Poseemos código sensible de seguridad —dos verificadores de webhooks con comparación en tiempo constante— que debemos acertar, probar y mantener correcto.
El balance, para un equipo pequeño con una superficie API simple y buenas pruebas, es positivo. Pero si tu backend toca veinte APIs de proveedores con superficies cambiantes, construir veinte clientes manualmente es un trato mucho peor, y un runtime Node que ejecute los SDK oficiales es una elección defendible. Si decides dar el salto, empieza donde deberíamos haber empezado nosotros: no con tu package.json, sino con npm ls --all y un análisis duro de lo que arrastran las dependencias de tus dependencias. El SDK que rompe tu despliegue nunca es el que importaste.
En Q2BSTUDIO, aplicamos esta misma filosofía a todos nuestros proyectos de cloud AWS/Azure, donde la elección del runtime y la gestión de dependencias son críticas para ofrecer soluciones escalables y seguras. También integramos servicios de ciberseguridad para proteger los webhooks y las claves de API, y utilizamos Business Intelligence con Power BI para monitorizar el comportamiento del sistema en tiempo real, detectando patrones de uso y posibles anomalías. Nuestros agentes IA se benefician de un runtime ligero que permite respuestas rápidas sin la sobrecarga de un contenedor pesado. La lección final: migrar a workerd no es un capricho técnico, sino una decisión estratégica que requiere auditar no solo tus importaciones, sino todo el árbol de dependencias transactivas. Cuando lo haces bien, ganas en velocidad, fiabilidad y control. Cuando no, aprendes —como nosotros— que lo que se rompe nunca es lo que esperas.





