El modelo dividido: por qué dos despliegues cuestan más de lo que parecen
La arquitectura típica de la era Pages ponía el front-end en un proyecto y la API en un Worker aparte, y esa frontera cobraba un peaje diario que casi nunca aparecía en la estimación: dos manifiestos, dos tuberías, dos historiales de versiones y ningún punto donde revertir ambos a la vez. Analizamos el desfase de versiones entre mitades, el enrutado por dominio compartido frente al preflight de CORS, las previsualizaciones que mentían y el catálogo de capacidades que nunca llegó a cruzar hacia Pages Functions.
Ninguna decisión de arquitectura se toma queriendo complicarse la vida; se toma resolviendo el problema que había delante. El equipo que en 2023 puso su front-end en Pages y su API en un Worker aparte no estaba equivocándose: estaba respondiendo con sensatez a un producto que servía archivos maravillosamente y ejecutaba código a medias. La división era la salida racional. Lo que ocurrió después es lo que ocurre siempre con las fronteras internas: el coste no llegó de golpe ni en forma de avería, sino goteando en decenas de pequeñas incomodidades que nadie apuntó en ningún sitio y que, sumadas al cabo de un año, valían más que el problema que la división había resuelto. Estudiar esa factura con detalle es la mejor forma de entender por qué la convergencia hacia un solo artefacto no fue un capricho de hoja de ruta, y de reconocer el mismo patrón la próxima vez que alguien proponga partir un sistema en dos por comodidad.
- Reconstruir la arquitectura dividida típica y las razones legítimas que la motivaban.
- Cuantificar la fricción operativa: dos manifiestos, dos tuberías y la imposibilidad de revertir en bloque.
- Diagnosticar el desfase de versiones entre front-end y API, y por qué las previsualizaciones lo ocultaban.
- Enumerar las capacidades de la plataforma que nunca llegaron a Pages Functions y forzaban la división.
La arquitectura de las dos mitades
El reparto era casi siempre el mismo. Un proyecto de Pages alojaba el resultado del framework de front-end y quizá alguna función pequeña para formularios. Un Worker independiente alojaba la API real, con sus bindings a base de datos, sus tareas programadas y su estado coordinado. Ambos convivían bajo el mismo dominio o en subdominios vecinos, y el navegador o el propio Pages hacía de puente entre ellos.
flowchart TD U[navegador] --> P[proyecto de Pages] U --> A[worker de api] P -->|assets y ssr| U A -->|json| U A --> DB[d1] A --> DO[durable objects] A --> Q[colas] P -.repositorio 1 y tuberia 1.-> CI1[despliegue de pages] A -.repositorio 2 y tuberia 2.-> CI2[despliegue de worker] style P fill:#89b4fa,color:#11111b style A fill:#f9e2af,color:#11111b style CI1 fill:#f38ba8,color:#11111b style CI2 fill:#f38ba8,color:#11111b
Ese diagrama tiene una propiedad que conviene mirar despacio: no hay ninguna caja que contenga a las otras dos. No existe la aplicación como unidad. Existen dos servicios que se comportan como una aplicación mientras alguien mantenga a mano el acuerdo entre ellos, y todo lo que sigue es consecuencia de esa ausencia.
Antes de criticar la división conviene reconocer que era razonable. El equipo que la adoptaba obtenía a cambio tres cosas concretas y valiosas: el flujo de compilación y previsualización de Pages para la parte que más se beneficia de él, el acceso íntegro a la plataforma para la parte que lo necesita, y la posibilidad de que dos personas distintas publicaran sin pisarse. Ninguna de las tres es despreciable, y quien las tenía delante en 2023 no disponía de una alternativa que las diera juntas. La crítica no es a la decisión, es al hecho de que su coste solo se manifiesta a partir del sexto mes.
Pages acabó admitiendo enlaces directos con otros Workers, lo que evitaba el salto por la red pública y el cruce de origen. Era una mejora real de latencia y de seguridad, y conviene usarla si mantienes un sistema así. Pero fíjate en lo que no arregla: siguen siendo dos artefactos con dos historiales, sigue habiendo una ventana de desfase en cada publicación y sigue sin existir una reversión que devuelva ambos a la vez. El enlace elimina el cable; no elimina la frontera.
La factura diaria
Conviene contar esta factura por partidas, porque el error habitual al evaluar una división es fijarse solo en la primera, que es la única que se ve el primer día.
La primera partida es la más visible y la que menos duele: duplicación de configuración. Dos manifiestos, dos listas de variables de entorno, dos juegos de secretos que hay que rotar en dos sitios, dos definiciones de dominio. Molesta, se automatiza a medias y se convive con ella.
Las tres partidas siguientes son las caras, porque no se resuelven con disciplina.
| Fricción | Qué falla exactamente | Cómo se parcheaba |
|---|---|---|
| Desfase de versiones | El front-end nuevo pide un campo que la API vieja no devuelve, o al revés | Versionar la API y desplegar siempre servidor antes que cliente |
| Reversión parcial | Volver atrás una mitad deja la otra en el futuro | Runbook manual con el orden inverso, ejecutado bajo presión |
| Cruce de origen | La API vive en otro origen que el sitio | Preflight de CORS | ruta compartida en el mismo dominio |
Ninguna de las tres se arregla siendo más ordenado, y esa es la prueba de que no son un problema de ejecución sino de estructura. Puedes reducir su frecuencia con procedimientos, pero no puedes hacerlas desaparecer mientras existan dos artefactos, porque las tres nacen exactamente de que existan dos.
El desfase merece detenimiento porque es el que produce incidentes de verdad. Cuando una aplicación es un solo artefacto, el contrato entre su código de cliente y su código de servidor se congela en el momento de compilar y viaja junto; no hay ventana en la que una mitad esté adelantada respecto a la otra. Cuando son dos artefactos, esa ventana existe siempre, dura lo que dure el segundo despliegue y se abre cada vez que alguien publica. La disciplina de desplegar primero el servidor y mantener compatibilidad hacia atrás funciona, pero es exactamente eso: disciplina, algo que un equipo sostiene hasta el viernes en que hay prisa.
La reversión es el caso donde la ausencia de una unidad se nota más. Revertir en Pages es instantáneo y fiable, pero solo revierte la mitad estática; la API sigue donde estaba. Un incidente causado por un cambio que tocó ambas mitades no tiene un botón que lo deshaga, tiene un procedimiento, y los procedimientos manuales ejecutados a las tres de la mañana son la definición operativa de un riesgo.
El cruce de origen parece la partida menor y es la que más código contamina. Si la API vive en un subdominio distinto, cada petición del navegador que lleve credenciales o cabeceras propias arrastra una petición previa de comprobación, y esa comprobación hay que atenderla en el servidor con una política que alguien tiene que mantener al día. La alternativa habitual era montar ambas mitades bajo el mismo dominio mediante una ruta, lo que eliminaba el problema del navegador a cambio de introducir otro: una regla de enrutado que vive en la configuración de la zona, fuera de los dos repositorios, y que por tanto no aparece en ninguna revisión de código ni viaja con ningún despliegue.
Cómo se veía desde el código
Conviene mirar el aspecto que tenía todo esto en la práctica, porque la fricción rara vez se documenta y en cambio queda impresa en el código. El patrón más común era una función de Pages que hacía de intermediaria hacia el Worker de la API para evitar el cruce de origen, y que en el camino tenía que reconstruir a mano la petición saliente.
export const onRequest: PagesFunction<Env> = async (context) => {
const url = new URL(context.request.url);
const destino = new URL(url.pathname.replace(/^\/api/, ""), context.env.API_BASE);
destino.search = url.search;
const saliente = new Request(destino, context.request);
saliente.headers.set("x-origen", "pages");
saliente.headers.set("x-version-cliente", context.env.VERSION_BUILD);
return fetch(saliente);
};
Ese archivo no aparece en ningún diagrama de arquitectura y sin embargo es la pieza que sostiene la ilusión de unidad. Nadie lo diseñó: fue creciendo a base de necesidades sucesivas, y con el tiempo acabó conteniendo reglas de reescritura, cabeceras de correlación y algún caso especial que ya nadie recuerda por qué está.
Hay tres cosas incómodas en esas pocas líneas, y las tres son consecuencia directa de la frontera. La primera es API_BASE: una dirección que hay que declarar por entorno y mantener sincronizada en dos proyectos, cuando dentro de un artefacto único no existiría en absoluto. La segunda es x-version-cliente: la cabecera que el equipo acaba añadiendo para poder correlacionar en los registros qué versión del front-end habló con qué versión de la API, un dato que el artefacto único tiene por construcción. La tercera es que, salvo que se use un enlace directo entre servicios, esa llamada es un salto de red real, con su latencia y su posibilidad de fallar, para hablar con un servicio que se despliega desde el escritorio de al lado.
Este es el efecto de segundo orden que casi nadie anticipaba. La previsualización de una rama de Pages levantaba el front-end nuevo, pero apuntaba a la API de producción, porque la API vivía en otro proyecto que no sabía nada de esa rama. Resultado: el mecanismo estrella del producto, la revisión visual antes de fusionar, dejaba de cubrir justamente los cambios que tocaban el contrato entre las dos mitades. Levantar una API de previsualización por rama era posible, pero exigía construir a mano toda la maquinaria de entornos efímeros que Pages regalaba para el front-end.
Un último apunte sobre esta factura, porque explica por qué tantos equipos la pagaron sin protestar. Ninguna de sus partidas produce una avería atribuible. El desfase se manifiesta como un error intermitente que no se reproduce; la reversión parcial, como un incidente que duró más de la cuenta; el cruce de origen, como un rato perdido configurando cabeceras. Cada episodio se archiva por separado y ninguno se contabiliza como coste de la arquitectura. Solo cuando alguien suma un año de esos episodios aparece la cifra, y para entonces la frontera lleva tanto tiempo ahí que se ha vuelto parte del paisaje.
Lo que nunca cruzó la frontera
Hay una lectura ingenua de esta historia según la cual bastaba con ir añadiendo capacidades a Pages Functions hasta igualar a Workers. No ocurrió, y la razón es estructural: Pages era un producto de despliegue de sitios al que se le había añadido cómputo, mientras que Workers es un producto de cómputo al que se le añadió el despliegue de sitios. Las capacidades que exigían que la unidad desplegada fuera un servicio de primera clase nunca encajaron.
Tareas programadas
Sin manejador scheduled propio. Cualquier trabajo periódico obligaba a un Worker aparte, aunque el resto del sistema viviera en Pages.
Clases de Durable Object
Podías enlazar con un objeto durable definido en otro sitio, pero no declarar la clase en tu proyecto. El estado coordinado vivía fuera por definición.
Consumidores de cola
Producir mensajes sí; consumirlos exigía el manejador queue de un Worker. La mitad asíncrona del sistema quedaba siempre al otro lado.
Despliegues graduales
Sin reparto por porcentaje entre versiones ni observabilidad por versión. El control fino de una publicación arriesgada no existía.
Añade a esa lista el RPC entre servicios con clases de punto de entrada, los Workflows durables y los contenedores, y el patrón se vuelve nítido: todo lo que convertía a un Worker en un servicio componible dentro de una arquitectura mayor quedó del lado de Workers. Pages podía ser cliente de la plataforma, pero no ciudadano de pleno derecho.
Ese matiz tiene una consecuencia que se subestima al planificar. La lista anterior no describe funciones que faltaban y podían añadirse una a una; describe un umbral. En cuanto tu sistema necesita una sola de ellas, la división deja de ser una elección y pasa a ser una obligación, y a partir de ahí toda la factura del apartado anterior se devenga entera, no en proporción a lo poco que uses la capacidad que te obligó a partirte. Un proyecto que solo quería una tarea nocturna de limpieza acababa manteniendo dos despliegues, dos juegos de secretos y un procedimiento de reversión manual por culpa de veinte líneas de código que se ejecutaban una vez al día.
Merece la pena nombrar también el caso inverso, porque existe y evita caer en el dogma. Hay sistemas en los que el front-end y la API pertenecen de verdad a equipos distintos, con calendarios distintos y un contrato estable pactado y versionado entre ellos. Ahí la división no solo es defendible, es la forma correcta de organizar el trabajo, y la factura de este capítulo se convierte en el precio razonable de la autonomía. El error no es dividir; es dividir por la costura tecnológica cuando la costura organizativa está en otro sitio o simplemente no existe.
Y hay un efecto de composición peor. Una vez existe el segundo servicio, cualquier necesidad futura tiende a caer en él, porque ahí sí caben todas las capacidades. El proyecto de Pages se va vaciando de responsabilidad hasta quedarse como una carpeta de archivos con una tubería de compilación propia, mientras el Worker acumula el sistema entero. Cuando alguien pregunta al cabo de un año por qué hay dos despliegues, la razón original ya no se sostiene, pero la frontera sigue ahí porque desmontarla exige una migración que nadie ha presupuestado.
La lección de fondo de este nivel no es sobre Pages, y por eso vale más que Pages: es sobre qué compras y qué vendes cada vez que decides que dos piezas de un sistema se desplieguen por separado. La intuición común dice que separar es prudente, porque desacopla, aísla fallos y deja que cada mitad evolucione a su ritmo, y esa intuición es correcta cuando las dos mitades tienen ciclos de vida genuinamente distintos y un contrato estable entre ellas. El error es aplicarla cuando lo único que difiere es la tecnología con que están escritas. Un front-end y la API que solo existe para servir a ese front-end no tienen ciclos de vida distintos: cambian juntos, en la misma propuesta de código, por la misma razón de negocio, y su contrato es cualquier cosa menos estable, porque se renegocia cada vez que se añade un campo a una pantalla. Separar su despliegue no las desacopla, porque el acoplamiento lógico sigue ahí intacto; lo único que hace es quitarte la herramienta que lo mantenía honesto. Y aquí está la parte que casi nadie calcula bien: lo que pierdes no es velocidad, es atomicidad. Mientras las dos mitades viajan en un artefacto, la consistencia entre ellas es una propiedad que el sistema te garantiza gratis, sin que tengas que pensarla ni escribir nada. En cuanto viajan por separado, esa garantía se convierte en trabajo tuyo, y es un trabajo que no admite atajos: versionado del contrato, compatibilidad hacia atrás en ambos sentidos, orden obligatorio de publicación, y un procedimiento de reversión que solo se ensaya en el peor momento posible. Ninguna de esas tareas aparece en la estimación del día en que se decide dividir, y todas aparecen en la factura de cada semana posterior. De ahí sale la regla práctica que conviene llevarse: la unidad de despliegue debería coincidir con la unidad de cambio, no con la unidad de tecnología. Si dos piezas cambian siempre a la vez, que se desplieguen a la vez, aunque una sea HTML y la otra SQL. Si de verdad cambian por separado y a ritmos distintos, entonces sí, sepáralas, pero hazlo sabiendo que acabas de firmar el mantenimiento de un contrato explícito y de un procedimiento de reversión que antes eran invisibles porque no existían. La convergencia de 2026 no hizo otra cosa que devolver esa elección a manos del ingeniero: hoy la frontera entre bytes servidos y cómputo ejecutado vive dentro de un mismo manifiesto y la mueves cuando quieras, en lugar de venir impuesta por qué producto elegiste el primer día.
- Dibuja tu sistema actual, o uno que conozcas, marcando cada unidad de despliegue con un color. Marca después, con otro color, cada conjunto de piezas que cambian siempre juntas. Observa si los colores coinciden.
- Localiza la ventana de desfase de tu último despliegue conjunto: cuántos segundos estuvo una mitad adelantada respecto a la otra, y qué habría pasado con una petición que cayera justo ahí.
- Escribe el procedimiento de reversión completo del sistema dividido, en orden, y crónometra cuánto tardarías en ejecutarlo sin ayuda. Compáralo con reapuntar una versión de un artefacto único.
- Toma una previsualización de rama de tu proyecto y comprueba contra qué API responde. Si es la de producción, enumera qué clase de errores es incapaz de detectar esa previsualización.
- Repasa la lista de capacidades que nunca cruzaron hacia Pages Functions y decide cuáles necesita tu proyecto hoy. Cada una es un argumento cuantificable para unificar el despliegue.