Este fue uno de los proyectos freelance más complejos que he ejecutado. Un cliente del sector industrial B2B me contrató para construir una plataforma completa desde cero: un directorio de empresas y servicios, una app móvil para usuarios en campo, un chatbot LLM integrado, mensajería interna, utilidades de gestión, y un pipeline de preparación de datos para ML.
En total: nueve aplicaciones distribuidas, con stacks distintos por bounded context, todo en producción en GCP.
Este post es el registro técnico de cómo lo diseñé, qué decidí y por qué, y qué aprendí operando un sistema poliglota a esta escala.
La plataforma completa — qué había que construir
El cliente tenía múltiples necesidades que no encajaban en un solo producto:
- Directorio B2B: marketplace donde empresas del sector podían publicar sus servicios y encontrar proveedores. Búsqueda, filtros, perfiles de empresa, documentos PDF de catálogos.
- App móvil: acceso desde campo para usuarios que no están frente a un computador.
- Chatbot LLM: asistente con acceso a los datos del directorio, capaz de responder preguntas sobre empresas y servicios.
- Mensajería interna: notificaciones y chat entre usuarios de la plataforma.
- Utilidades de gestión: módulo de torneos internos, menú digital para instalaciones, APIs de datos de empresas.
- Pipeline ML: preparación de datasets para entrenar modelos con datos propios del directorio.
No es un MVP. Es un ecosistema completo que el cliente necesitaba operativo.
Arquitectura del sistema
graph TB
subgraph "Clientes"
WEB["Angular 19\n(Web app)"]
MOB["React Native 0.81\n(iOS + Android)"]
end
subgraph "Core — Directorio B2B"
DIR["api-directorio\n(Fastify + Node.js)\nEmpresas · Servicios · Búsqueda"]
DIR_PDF["api-directorio-pdf\n(Python FastAPI)\nGeneración PDFs · GCS"]
end
subgraph "LLM / AI"
CHAT["chat-api\n(Python FastAPI)\nVertexAI · BigQuery ML"]
PYAI["py-ai\n(Python)\nDataset prep · JSONL · GCP upload"]
end
subgraph "Mensajería"
MSG["api-messages\n(Node.js + Express)\nFirestore real-time"]
NOTIF["notifications-service\n(Node.js + Express)\nFCM · Push · Email"]
end
subgraph "Utilidades"
GOLF["torneo-golf\n(PHP + PHPSpreadsheet)"]
MENU["menu-digital\n(PHP + widget embed)"]
end
subgraph "GCP Infra"
FS["Firestore"]
GCS["Cloud Storage\n(GCS)"]
BQ["BigQuery"]
VERTEX["Vertex AI"]
CR["Cloud Run"]
end
WEB --> DIR
WEB --> CHAT
WEB --> MSG
MOB --> DIR
MOB --> MSG
MOB --> NOTIF
DIR --> GCS
DIR_PDF --> GCS
DIR --> DIR_PDF
CHAT --> VERTEX
CHAT --> BQ
PYAI --> BQ
MSG --> FS
NOTIF --> FS
NOTIF --> MOB
CR -. hospeda .-> DIR
CR -. hospeda .-> CHAT
CR -. hospeda .-> MSG
CR -. hospeda .-> NOTIF
Nueve aplicaciones en producción. El núcleo es el directorio — ahí está la mayor concentración de lógica de negocio (~11,500 líneas). Todo el resto orbita alrededor de ese core.
La decisión de stack más no obvia: Fastify para el directorio
El instinto en Node.js es Express. Es lo que más conozco, tiene el ecosistema más amplio, y hay miles de ejemplos.
Para el directorio elegí Fastify. Hay una razón concreta: throughput bajo carga de búsqueda.
El directorio tiene un patrón de uso específico: muchas consultas de búsqueda y listado concurrentes, con relativamente pocas escrituras. Esas queries de lectura involucran filtros múltiples (tipo de empresa, ubicación, servicios ofrecidos, certificaciones). Con Express, bajo carga moderada, empecé a ver latencia acumularse en pruebas de carga.
Fastify tiene overhead de routing significativamente menor que Express y serialización JSON optimizada. En benchmarks propios con el schema real del directorio, Fastify manejó ~40% más requests por segundo que Express en el mismo hardware. Para un directorio B2B con carga variable (picos cuando empresas buscan proveedores), ese margen importa.
El trade-off es validación de esquemas más estricta — Fastify usa JSON Schema para validar requests y serializar responses. Más verboso al definir endpoints, pero el beneficio es doble: validación automática en el servidor y documentación auto-generada con @fastify/swagger.
Servicio de PDFs en Python — por qué separarlo del directorio
El directorio necesitaba generar catálogos PDF con el perfil de cada empresa: logo, descripción, lista de servicios, datos de contacto, certificaciones. Layout específico con tablas y margenes del cliente.
Puse eso en un servicio separado en Python FastAPI, no en el servicio Fastify principal.
La razón es la misma que en otros proyectos: las librerías Python para generación de PDFs (reportlab, Pillow) tienen mejor soporte para layouts complejos que las alternativas en Node. pdfkit y puppeteer hacen PDFs en Node, pero cuando el layout tiene reglas específicas de posicionamiento y tablas con contenido dinámico, Python gana en madurez de herramientas.
El servicio PDF recibe los datos del directorio, genera el archivo, lo sube a GCS, y devuelve la URL firmada. El directorio Fastify llama a ese servicio cuando un usuario solicita el catálogo. El cliente ve una URL de descarga; no sabe que hay dos servicios detrás.
LLM integration — VertexAI + BigQuery
El chatbot LLM fue la parte más interesante técnicamente.
El cliente quería que los usuarios pudieran hacer preguntas en lenguaje natural sobre el directorio: “¿qué empresas ofrecen servicios de inspección en Bogotá?”, “muéstrame proveedores con certificación ISO 9001”. Ese tipo de consultas no mapean limpio a una búsqueda por filtros.
Stack elegido: Python FastAPI + VertexAI + BigQuery.
Por qué VertexAI y no OpenAI: el cliente ya tenía cuenta GCP y los datos del directorio estaban en BigQuery. VertexAI permite hacer embedding y consultas sobre esos datos sin mover nada fuera del ecosistema GCP. Eso elimina latencia de red entre servicios y simplifica la surface de seguridad — los datos del directorio nunca salen de GCP para ser procesados.
El flujo de una consulta LLM:
Usuario pregunta en lenguaje natural
↓
chat-api recibe query
↓
Embedding de la query (VertexAI text-embedding)
↓
Búsqueda vectorial en BigQuery ML
(comparación coseno contra embeddings pre-computados de empresas)
↓
Top-K empresas relevantes como contexto
↓
Prompt a VertexAI Gemini con contexto + query original
↓
Respuesta estructurada al usuario
El pipeline de py-ai se encarga de la preparación del dataset: toma los datos del directorio (JSON desde la API + PDFs de catálogos), los convierte a JSONL, genera embeddings con VertexAI, y los sube a BigQuery. Eso se ejecuta como batch cuando hay actualizaciones en el directorio. El chat-api siempre consulta el dataset pre-computado, nunca genera embeddings on-the-fly durante una consulta — eso hubiera sido demasiado lento para la latencia esperada.
App móvil — React Native + Expo EAS
Los usuarios en campo necesitaban acceso desde teléfono. Las opciones eran:
- Web responsive (lo más simple)
- React Native bare
- React Native + Expo Managed Workflow
- Nativo (Swift/Kotlin)
Elegí React Native 0.81 + Expo 54 con Managed Workflow y EAS Build.
La web responsive la descarté porque necesitábamos notificaciones push nativas y acceso offline parcial para zonas con señal intermitente. Nativo hubiera sido dos codebases (iOS + Android) con dos veces el esfuerzo de mantenimiento para una sola persona.
React Native bare vs Expo: Expo Managed Workflow con EAS Build es la respuesta correcta para proyectos freelance donde no tienes un equipo mobile dedicado. Las razones:
EAS Build: CI/CD para mobile sin configurar Xcode ni Android Studio en el servidor. El build corre en la nube de Expo. eas build --platform all genera los binarios listos para publicar en App Store y Play Store. Sin esa abstracción, configurar un pipeline de builds mobile desde cero hubiera costado días.
EAS Submit: automatiza el submission a App Store Connect y Google Play. Un comando, credenciales configuradas, listo.
OTA Updates: las actualizaciones de JavaScript se pueden hacer sin pasar por revisión de la tienda. Cuando hay que corregir un bug en la UI o cambiar lógica de negocio, hago eas update y todos los usuarios reciben el cambio en el próximo launch. Sin OTA, cada fix de UI requeriría un ciclo completo de revisión (días en App Store, horas en Play Store).
La integración con FCM (Firebase Cloud Messaging) para push notifications se maneja a través de expo-notifications. El notifications-service en el backend envía a través de FCM, que distribuye a Android e iOS.
Mensajería en tiempo real — Firestore
Para la mensajería interna usé el stack Firebase que ya conocía: Firestore para persistencia y real-time, FCM para push.
La decisión de Firestore aquí fue pragmática: el cliente no tenía equipo DevOps, y Firestore escala automáticamente sin operaciones. Para un sistema de mensajería donde los bounded contexts están bien definidos, el lock-in de Firestore es un trade-off aceptable a cambio de cero infraestructura que mantener.
El patrón onSnapshot vs polling ya lo tenía resuelto de proyectos anteriores:
const unsubscribe = db.collection('messages')
.where('conversationId', '==', convId)
.orderBy('createdAt', 'desc')
.limit(50)
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') handleNewMessage(change.doc);
});
});
El listener recibe solo deltas, no el snapshot completo. Eso importa cuando una conversación tiene historial largo.
Lo que aprendí en este proyecto que no esperaba: los edge cases de usuarios con múltiples dispositivos. Un usuario con la app móvil y la web abierta al mismo tiempo puede generar estados de “leído/no leído” inconsistentes si no manejas el source of truth correctamente. El estado de notificación vive en Firestore (autoridad), no en el cliente.
El stack poliglota — defendiendo la decisión
Nueve aplicaciones con tres runtimes: Node.js, Python, PHP.
La pregunta obvia es: ¿por qué no estandarizar en un solo lenguaje?
La respuesta está en los bounded contexts. Cada aplicación tiene una responsabilidad clara y un conjunto de herramientas óptimo para esa responsabilidad:
- Directorio (Fastify/Node): API de alto throughput con muchas lecturas concurrentes. Node.js gana en I/O bound.
- PDFs (Python/FastAPI): generación de documentos con layout preciso. Librerías Python superiores.
- LLM/ML (Python/FastAPI): VertexAI SDK, BigQuery ML, numpy. Python es el estándar de facto.
- Dataset prep (Python): transformaciones de datos, embeddings. Python sin discusión.
- Mensajería (Node/Express): Firebase Admin SDK tiene mejor soporte en Node que en Python.
- Utilidades (PHP): módulos legacy del cliente ya existían en PHP. Reescribirlos en otro lenguaje hubiera sido tiempo sin valor de negocio.
El costo del stack poliglota es real: hay que mantener dependencias, versiones, y convenciones en tres ecosistemas distintos. Lo mitigo con Docker por servicio (cada uno tiene su imagen con su runtime específico) y docker-compose para levantar el sistema completo en desarrollo local.
Infra y CI/CD
Todo el stack vive en GCP:
- Cloud Run: contenedores de los servicios Node y Python. Auto-scaling, sin instancias que gestionar.
- GCS: documentos, catálogos PDF, assets estáticos.
- BigQuery: datos del directorio + embeddings para el pipeline LLM.
- Vertex AI: modelos de embedding y generación de texto.
- Firestore: mensajería en tiempo real.
- FCM: push notifications.
La consistencia del ecosistema GCP no fue un accidente. IAM centralizado, billing unificado, y los servicios se comunican con identidades de servicio sin credentials en código.
Para CI/CD de la app móvil, EAS Build conectado al repositorio dispara builds en cada push a la rama de producción. Para los servicios backend, Cloud Build con trigger en push a main hace el deploy a Cloud Run.
Números del proyecto
| Aplicación | Stack | LOC aprox |
|---|---|---|
| api-directorio | Fastify + Node.js | ~11,500 |
| app-móvil | React Native + Expo 54 | ~3,200 |
| py-ai | Python (dataset pipeline) | ~1,200 |
| chat-api | Python FastAPI + VertexAI | ~1,500 |
| api-directorio-pdf | Python FastAPI | ~800 |
| api-messages | Node.js + Express + Firebase | ~800 |
| notifications-service | Node.js + Express + FCM | ~375 |
| torneo-golf | PHP + PHPSpreadsheet | ~900 |
| menu-digital | PHP | ~750 |
Total: ~21,000+ líneas distribuidas en 9 aplicaciones.
Lo que aprendí
Los proyectos poliglotas son manejables con buenos boundaries. El riesgo no es tener múltiples lenguajes — es tener múltiples lenguajes sin claridad de por qué existe cada uno. Cuando cada servicio tiene una responsabilidad definida y hay una razón documentada para su stack, el equipo (o yo solo) puede navegar el sistema sin perderse.
EAS cambió mi relación con mobile. Antes de EAS, el overhead de CI/CD móvil me hacía evitar proyectos que requirieran app nativa. Con EAS, el pipeline de build/submit/OTA es tan predecible como un deploy de backend. Eso abre proyectos que antes hubiera rechazado.
VertexAI + BigQuery es un stack sólido para LLM con datos propios. No es la opción más simple ni la más barata para un caso de uso simple. Pero cuando los datos viven en GCP y el cliente ya está en ese ecosistema, la integración nativa reduce la complejidad operacional de forma significativa. Los embeddings pre-computados en BigQuery ML para RAG son estables y la latencia de consulta es predecible.
El servicio más complejo no es el más “innovador”. El directorio B2B en Fastify es la aplicación de mayor tamaño (~11,500 líneas) y la que más tiempo tomó. No tiene LLM ni mobile — tiene lógica de negocio real, búsqueda con múltiples filtros, gestión de archivos, generación de documentos, y un modelo de datos que refleja cómo funciona un sector industrial específico. La complejidad técnica viene del dominio, no del stack.