Volver al blog
Inteligencia Artificial

Nexo: anatomía técnica de nuestro SaaS multi-tenant — Laravel 12 + React + Inertia.js + Brain (agentes IA)

Nexo — arquitectura de un SaaS multi-tenant con Laravel 12, React + Inertia.js y agentes IA Brain

Nexo: anatomía técnica de nuestro SaaS multi-tenant — Laravel 12 + React + Inertia.js + Brain (agentes IA)

Por el equipo de Kiwop · Agencia Digital especializada en Desarrollo de Software e Inteligencia Artificial aplicada · Publicado el 19 de abril de 2026 · Última actualización: 19 de abril de 2026

TL;DR — Nexo es el SaaS operativo de Kiwop (HR, CRM, proyectos, facturación, nóminas) sobre Laravel 12 + React + Inertia.js, con agentes IA propios (Brain). Tres decisiones lo definen: jerarquía Organization → Workspace → Member con toggles de módulos por workspace, Inertia.js como puente entre monolito y SPA, y coexistencia controlada entre rutas legacy (Blade) y nuevas (Inertia) mediante un middleware guardián que bloquea escrituras antiguas.

Nexo — arquitectura de un SaaS multi-tenant con Laravel 12, React + Inertia.js y agentes IA Brain

En Kiwop llevamos años construyendo software a medida para clientes de Europa y USA, pero Nexo es distinto: es el producto que usamos cada día nosotros mismos para operar la agencia, y a la vez el SaaS que ofrecemos como caso de éxito a empresas B2B mid-market que necesitan centralizar HR, CRM, proyectos y facturación en una sola plataforma. Cuando construyes algo que tu propio equipo usa a diario, la arquitectura deja de ser un ejercicio teórico: cada decisión se paga o se cobra cada lunes por la mañana.

Este artículo entra al detalle técnico. Es la documentación del estado real del sistema a abril de 2026 — decisiones, trade-offs y cosas que haríamos distinto. Encaja en el mismo cluster que nuestro post sobre reconstruir un asistente personal con Claude Code y MCP, la guía para construir tu PA con Claude Code, los patrones y antipatrones de agentes IA en producción y los protocolos MCP, WebMCP y A2A en 2026. Si esos posts tratan agentes fuera de la app, Nexo es la demostración de cómo metes agentes dentro de una aplicación que ya tiene un dominio rico, datos reales y un tenancy serio.

Qué es Nexo y para qué lo construimos

Nexo nace de un problema recurrente en empresas B2B de entre diez y doscientos empleados: la operativa diaria vive repartida en media docena de herramientas desconectadas. Control horario en una app, CRM en otra, proyectos en ClickUp, facturación en un ERP, nóminas en la gestoría, docs en Notion. Cada herramienta funciona bien por separado, pero nadie ve la operación como un todo, los datos de cliente se duplican entre sistemas, y cada informe requiere exportar CSVs de cuatro sitios y cruzarlos a mano.

En Kiwop sufríamos ese problema. Construimos Nexo primero para nosotros y luego lo convertimos en producto. A día de hoy cubre cinco módulos:

  • HR: control horario (fichajes con geolocalización y trazabilidad legal de cuatro años), gestión de vacaciones y ausencias, fichas de empleado, equipos y festivos.
  • CRM: empresas, contactos, leads con pipeline, actividades comerciales y campañas outbound.
  • Projects: proyectos, tareas con tiempo registrable, comentarios, adjuntos y sesiones de tiempo.
  • Billing: contratos, hitos de facturación, avales de licitación, sincronización bidireccional con Holded ERP.
  • Payroll: nóminas con cálculo legal español, soporte para varios convenios colectivos (TIC, Comercio Catalunya), exportación SILTRA.

Encima de todo eso viven dos piezas que lo separan de un SaaS operativo clásico: un subsistema de extracción de contratos con IA (subes un PDF de contrato, la IA rellena el alta) y un motor de agentes IA propio llamado Brain, que permite configurar protocolos multi-paso para que la plataforma ejecute trabajo autónomo sobre los datos del workspace.

Nexo corre en producción en nexo.kiwop.com con decenas de workspaces activos. No son miles: es un SaaS B2B mid-market, no una app de consumo. Y se muestra en nuestra web como caso de éxito aunque sea producto nuestro, porque la historia técnica es exactamente la misma que contaríamos si el cliente fuera externo.

Por qué Laravel + Inertia.js + React (y no SPA pura)

La primera decisión grande fue el esqueleto del frontend. En 2025 el consenso por inercia era "SPA en React/Vue consumiendo API REST o GraphQL". Nosotros fuimos por otra vía: Laravel 12 en el backend, React 18 + TypeScript en el frontend, e Inertia.js como puente. No una SPA separada. Un monolito moderno con vistas React renderizadas desde controladores Laravel.

Cuando separas backend y frontend en dos repositorios conectados por API, ganas una cosa y pierdes cinco. Ganas libertad para poner otro frontend mañana (una app móvil, otra marca con la misma API). Pierdes: autenticación unificada, validación compartida, SEO trivial, deploy atómico, estado de sesión server-side, y la capacidad de iterar un feature en un solo commit. Para una aplicación autenticada interna como Nexo, esas cinco cosas pesan mucho más que la flexibilidad teórica.

Inertia resuelve el encaje. Los controladores Laravel, en lugar de Blade o JSON, devuelven Inertia::render('NombrePagina', $props). Eso renderiza un componente React concreto (resources/js/Pages/Workspace/Dashboard.tsx) con las props que el backend ya calculó. Navegar es un fetch interno que intercambia componente y props sin recargar: UX de SPA, verdad en el servidor, autenticación por cookies y middleware Laravel de toda la vida.

El middleware HandleInertiaRequests comparte props globales (usuario, workspace, rol, flags) a todas las páginas, así ningún componente pide ese estado en cada página ni montamos un Redux/Zustand para algo que el servidor ya sabe. Deploy único: git pull, composer install, npm run build, limpieza de cachés, sin ventanas de incoherencia entre API y cliente.

El coste es que al abrir Nexo a integradores externos necesitamos API formal paralela. La montamos pronto: REST API `/api/v1` con Laravel Sanctum (tareas, proyectos, registros, docs, leads, billing, webhooks), viviendo junto a las rutas Inertia. Frontend propio por Inertia, integradores por REST con tokens.

Nexo — stack y capas: Laravel 12 + Inertia.js + React + MySQL + agentes Brain

El modelo multi-tenant: Organization → Workspace → Member

La segunda decisión estructural es cómo haces multi-tenancy. Dos escuelas enfrentadas: shared database con columna de tenant contra schema per tenant. Elegimos la primera, con una jerarquía de tres niveles.

  • Organization: agrupación opcional para cuentas enterprise con varios workspaces bajo una misma marca.
  • Workspace: la unidad real de tenancy. Datos, configuración, módulos e identidad visual propios. Identificado por slug en la URL: /w/kiwop/dashboard.
  • WorkspaceMember: tabla pivote que conecta usuarios y workspaces con un rol (owner, admin, comercial, operations, member, viewer).

Elegimos shared database con columna workspace_id por tres razones pragmáticas: evolucionar el modelo sin migrar N esquemas, analíticas cruzadas de producto sin federar queries, y una operación de backup/restore única. El coste es que un bug que omita un filtro por workspace_id puede, en teoría, cruzar datos entre tenants. Lo mitigamos con dos piezas.

La primera es el middleware `EnsureWorkspaceContext`, que resuelve el slug de la URL al modelo Workspace, verifica membresía del usuario y, si el workspace pertenece a una organización, también la membresía en ella. Falla con 403 si algo no cuadra. Corre en todas las rutas bajo /w/{workspaceSlug}/*.

La segunda es el trait `WorkspaceScoped` aplicado a los modelos Eloquent sensibles (BrainProtocol, BrainProtocolRun, etc.), de modo que cualquier query aplica por defecto where workspace_id = ?. Desactivarlo requiere hacerlo explícito, y eso se ve en code review.

Hay un detalle operativo que documentamos como regla absoluta: los controladores de workspace deben incluir string $workspaceSlug como segundo parámetro (tras Request $request) antes de cualquier otro. Laravel inyecta parámetros posicionalmente, y omitir el slug hace que el siguiente parámetro ($id) reciba el slug y la request muera en un 404 silencioso muy difícil de diagnosticar. La ruta index funciona, lo que hace que el bug solo aparezca al abrir el primer detalle.

Nexo — jerarquía multi-tenant: Organization > Workspace > WorkspaceMember > User

La siguiente tabla resume las decisiones arquitectónicas troncales que tomamos en Nexo, con la razón y el trade-off asumido. Si estás diseñando un sistema parecido, esta tabla es probablemente la parte del artículo que te conviene copiar-pegar en un documento interno.

Module toggle system: activar/desactivar features por workspace

Cada workspace activa o desactiva módulos de forma independiente. El mecanismo: una columna JSON enabled_modules en la tabla workspaces que mapea módulo a booleano. La lista es cerrada y vive en código como constante Workspace::MODULES (no dejamos que el usuario cree módulos nuevos en tiempo de ejecución). Incluye los bloques core (dashboard siempre activo, fichaje, registros, solicitudes), analítica (rentabilidad, informes), trabajo (projects, tasks), ventas (leads, outbound, licitaciones), finanzas (billing_contracts, billing, contabilidad), herramientas (booking, decision_requests, chat), conocimiento (docs), settings (branding, notifications, settings), HR (employees, practicas, personal_documents, payroll) y módulos más recientes como chatbot, pulse_surveys, proactive_agents, capacity_planner, project_pulses y meetings.

Dos puntos consultan el toggle: el método isModuleEnabled('leads') del modelo, y sobre todo el middleware `EnsureModuleEnabled` aplicado a los grupos de rutas:

El middleware comprueba dos cosas en orden: (1) si el módulo está activado para el workspace, y (2) si el rol del usuario en ese workspace puede acceder al módulo. Ante cualquier fallo, devuelve 404 (no 403, para no filtrar la existencia de la ruta). Es una pequeña medida de security through obscurity que, combinada con las otras capas, sube el listón del reconocimiento para un atacante interno.

Por qué no usamos feature flags clásicos (LaunchDarkly, Unleash, Flagsmith): nuestros toggles no son experimentos A/B ni roll-outs graduales, son configuración de producto por tenant. Un cliente sin módulo Payroll no lo va a tener hasta que lo contrate. Meter un SaaS externo para gestionar eso sería complejidad gratuita.

El módulo branding merece mención aparte: cuando está activo, el workspace puede personalizar su color primario (CSS variable --workspace-primary), subir logo y adaptar la identidad visual. Útil para white-label o clientes enterprise que quieren la marca interna. El CSS base se mantiene, solo cambia el acento.

RBAC: roles + permisos sin bibliotecas externas

Para autorización, Nexo no usa Spatie Laravel-permission ni ninguna otra librería. Tenemos sistema RBAC propio: tablas roles, permissions y role_permissions, con los roles fijos (owner, admin, comercial, operations, member, viewer) y permisos granulares definidos en RbacSeeder. Tres razones para no usar librería.

Primero, el dominio de permisos está acoplado a conceptos propios: workspace, organización, módulos, acciones sobre entidades. No es "usuario puede hacer X", sino "este usuario, en este workspace, con este rol, puede hacer X sobre Y". Con una biblioteca genérica habríamos terminado reescribiendo la mitad de las capas encima.

Segundo, el Gate de Laravel ya resuelve la autorización. Definimos gates en AuthServiceProvider apuntando a policies (DocumentationPolicy::viewCategory) y usamos Gate::allows(...) o @can(...) en las vistas. Lo único que añadimos es la resolución del rol de workspace: $workspace->getUserRole($user) y $workspace->canRoleAccessModule($role, 'leads'), cacheado en $request->attributes durante el ciclo de vida de la request.

Tercero, tenemos un mecanismo de simulación ("ver la app como otro usuario", útil en soporte) que interactúa con el RBAC de forma específica: cuando el admin simula, el bypass habitual "admin lo puede todo" se desactiva a propósito, y ves la app tal como la ve el usuario simulado. Eso vive en EnsureModuleEnabled con $request->attributes->get('is_simulating', false). Encima de una biblioteca externa habría sido frágil.

Legacy coexistence: Blade antiguo + Inertia nuevo en paralelo

Nexo tiene una historia previa. La versión original de la aplicación estaba construida en Laravel 10 con Blade + jQuery + Bootstrap. El frontend Inertia/React es posterior. La migración no se hizo de golpe: todavía queda código Blade vivo en producción. Esa coexistencia es la realidad de cualquier aplicación real que lleva varios años en producción.

La pregunta es cómo evitas que esa coexistencia se convierta en un problema. Nuestra respuesta es el middleware `LegacyRouteGuard`, registrado como legacy.guard, aplicado a todas las rutas antiguas. Hace tres cosas:

  1. Loguea cualquier acceso a rutas legacy en storage/logs/legacy-routes-*.log. Telemetría real de qué rutas antiguas siguen vivas, guía la priorización.
  2. Bloquea POST/PUT/PATCH/DELETE con 409 Conflict y mensaje "Legacy interface is read-only". Ningún cambio de datos va por la vía antigua.
  3. Redirige GETs a la equivalente nueva cuando hay un mapeo definido. El redirectMap asocia ruta legacy a ruta workspace ('admin.horarios' => 'workspace.registros.index', etc.) y resuelve parámetros (workspaceSlug, articleId, contract) en el momento.

La regla clave es "GETs abiertos, writes cerrados". Si bloqueas todo, rompes bookmarks y enlaces viejos en correos. Si permites writes, tienes dos caminos para modificar los mismos datos y no puedes garantizar que las validaciones, eventos y logs de auditoría del camino nuevo se disparen. La combinación "lee por el camino viejo, escribe solo por el nuevo" permite no romper bookmarks mientras migramos vistas una a una sin miedo.

Hay una excepción: las rutas de simulación de admin (/admin/simulate-employee, /admin/stop-simulation, /admin/toggle-view-mode) no llevan el guardián, porque son puertas legítimas de soporte. El resto, sí.

Es un patrón replicable: si tienes una app Laravel de 2019 con Blade y quieres migrar a Inertia sin parar seis meses el negocio, añade middleware guardián, bloquea writes, loguea accesos, mapea redirects, y ataca la migración por frecuencia de acceso real (no por orden alfabético ni por gusto estético).

Nexo — coexistencia legacy Blade y nuevo Inertia con middleware guardián

Brain — el sistema de agentes IA de Nexo

Aquí entra la pieza más propia de Nexo: Brain, un motor de agentes IA multi-paso que vive dentro de la aplicación. No es un wrapper de LangChain ni un framework genérico. Es un motor diseñado para ejecutar automatizaciones sobre los datos de un workspace, con límites explícitos y trazabilidad total.

El dominio son dos modelos: `BrainProtocol` (definición: nombre, descripción, pasos, configuración) y `BrainProtocolRun` (ejecución: contexto, resultados paso a paso, estado, tokens). Ambos viven bajo workspace_id con el trait WorkspaceScoped, así que nunca se cruzan entre tenants.

El motor `BrainProtocolEngine` procesa pasos secuencialmente con un contexto mutable en memoria. Cada paso lee del contexto, ejecuta su lógica y escribe bajo una output_key que los siguientes pasos pueden leer. Aplica dos límites duros escritos a fuego:

Un protocolo con más de diez pasos se trunca al ejecutar. Si el contexto acumulado supera 300.000 caracteres, logueamos warning y antes de llamar al modelo lo truncamos con un marcador [CONTEXTO TRUNCADO POR LÍMITE DE TAMAÑO]. Esos dos límites son la barrera contra el riesgo más real de un agente: la explosión combinatoria de contexto y coste cuando itera sobre sus propios outputs.

Brain soporta seis tipos de paso:

Nexo — Brain Protocol Engine: seis tipos de paso ejecutados secuencialmente sobre un contexto mutable

El paso ai_call es el que más trabajo hace. Lee las claves marcadas como input_keys, las serializa a JSON bajo cabeceras ### clave y las añade a una sección ## DATOS DISPONIBLES concatenada al prompt. Eso hace explícito el contrato prompt-contexto, evita que el modelo invente datos fuera de la sección, y facilita el debug: el contexto exacto queda en el audit trail.

El paso action es lo que hace a Brain "un agente" y no un pipeline. Tipos soportados: create_task, create_proposal, save_memory, alert. Cada uno resuelve parámetros con plantillas {{key.subkey}} contra el contexto: el título de una tarea puede ser {{ai_summary.content}} y el motor lo sustituye antes de ejecutar. Conecta el razonamiento del modelo con la acción real sobre el dominio.

El paso condition es el cortafuegos. Con skip_remaining_if_false: true, si la condición no se cumple, el resto del protocolo se salta y los pasos quedan marcados como skipped_remaining. Permite protocolos del tipo "si no hay nada que hacer, no gastes tokens".

Ejemplo: protocolo de onboarding de leads. (1) connector_fetch de leads nuevos de las últimas 24h. (2) condition que aborta si la lista está vacía. (3) data_filter que se queda con leads de score > 60, límite 20. (4) ai_call que genera un primer borrador de correo. (5) action tipo create_proposal que deja la propuesta en la bandeja del comercial — no un correo enviado, porque las acciones con efecto externo pasan por aprobación humana.

Todo queda registrado en BrainProtocolRun: contexto final, resultados de cada paso con duración y estado, tokens, usuario, fuente del trigger (manual, schedule, connector_update, agent_alert) y timestamps. Ese audit trail es la tabla por la que un compliance officer puede auditar qué hizo la IA sobre los datos del cliente. Sin él, un SaaS con agentes no es auditable; con él, lo es al grano más fino. Brain es nuestra materialización interna de muchos de los patrones que documentamos en agentes IA en producción: patrones y antipatrones para 2026.

Extracción de contratos con IA + anti-alucinación

La otra pieza importante de IA en Nexo es anterior a Brain y resuelve un caso concreto: dar de alta un contrato de facturación a partir de su PDF. El trabajo manual de copiar nombre de cliente, importe, fechas, datos e-Fact, avales y hitos es pegajoso y propenso a erratas.

El subsistema `AiContractExtraction` resuelve ese flujo. Un endpoint POST /billing/contracts/ai-extract recibe el PDF (máximo 10MB), lo pasa por PdfTextExtractor (basado en smalot/pdfparser), y lo entrega al extractor elegido por AiExtractorFactory. La factory elige entre OpenAiContractExtractor (GPT-4o en su versión estable más reciente, configurable por OPENAI_MODEL) o su equivalente Anthropic según AI_PROVIDER. La interfaz ContractExtractorInterface garantiza que cambiar de proveedor es un flag.

El prompt está optimizado para contratos en castellano, con reglas anti-alucinación explícitas. La respuesta es JSON estructurado donde cada campo es un objeto con value, confidence (0-1) y evidence (cita literal del texto del PDF). Eso cambia la UX radicalmente: el formulario se autorrellena solo cuando confidence >= 0.5, los campos rellenados aparecen con borde azul claro y tooltip "Rellenado por IA (confianza: 85%)", y un acordeón "¿qué encontró la IA?" muestra la evidencia literal. El humano nunca confía ciegamente: revisa, ajusta y guarda.

Las reglas anti-alucinación del system prompt son el núcleo de por qué esto funciona. Si la IA no encuentra un campo, el valor debe ser null y confidence debe ser 0. Si infiere un valor por contexto (asumir 21% de IVA cuando no está desglosado), debe bajar la confidence y añadir una entrada a warnings. Los warnings se muestran antes de guardar: "No se encontró IVA desglosado, asumiendo 21%", "Fecha de fin no especificada claramente". La combinación confidence + evidence + warnings + revisión humana obligatoria es lo que hace que una extracción con LLM sea operable en un entorno donde un error te lleva a emitir mal una factura.

Todo queda en la tabla ai_contract_extractions: quién subió, cuándo, qué archivo (con hash SHA256; no guardamos el texto completo, es importante para privacidad), qué proveedor, qué modelo, tokens, coste estimado, resultado JSON, warnings, estado. Si cinco meses después alguien pregunta "¿cómo llegaron estos datos al contrato?", la respuesta está en la base de datos.

El motor general detrás — el que permite swap entre OpenAI y Anthropic con un flag, el que gestiona límites de presupuesto (AiBudgetGuard), el que resuelve qué conexión usar según workspace (AiConnectionResolver) — es el `AiGateway`, el mismo que usan los ai_call de Brain. Centralizar la salida a LLMs da tres cosas: punto único para rate limits y budgets, punto único para loguear tokens por feature (chat, agent, extraction), y punto único para cambiar proveedor sin tocar código de negocio. Si mañana probamos un Sonnet 5 o un GPT-6, el cambio vive en una sola línea.

Esta abstracción la aplicamos también en proyectos de cliente bajo integración de LLMs y desarrollo de agentes IA. Centralizar la puerta de salida al modelo es uno de los primeros consejos que damos a quien integra IA en un producto en producción.

Integración con Holded ERP (bidireccional)

Nexo no reemplaza al ERP de facturación. Reemplaza al panel de gestión que mira al ERP. Para facturación real seguimos usando Holded y sincronizamos bidireccionalmente con él. La integración vive bajo app/Services/Holded/ con cuatro piezas: HoldedClient (cliente HTTP), HoldedSyncService (orquestador), HoldedStatusMapper (mapeo de estados) y SyncResult / BatchSyncResult (resultados tipados).

La sincronización sigue el patrón clásico de integración con ERP externo: jobs en queue con reintentos, sincronización programada cada 15 minutos, y un Artisan command holded:sync-invoices --sync para forzar una ronda manual. La dirección bidireccional significa dos flujos: de Nexo a Holded cuando un hito se marca como "emitido" (se crea la factura) o "pagado" manualmente, y de Holded a Nexo cuando un cliente paga una factura en el ERP y el cambio de estado vuelve en la siguiente ronda.

La gestión de conflictos es donde estas integraciones se ensucian. Nuestra regla: "Holded gana en estados financieros, Nexo gana en metadata comercial". El estado de pago lo decide Holded (sistema financiero oficial). Las etiquetas internas, notas y vínculos a proyectos los decide Nexo. Esa división reduce los conflictos a un subconjunto manejable donde una de las dos partes es siempre autoritativa.

HOLDED_ENABLED activa o desactiva la integración por entorno. La configuración se hace a nivel de workspace, no globalmente — cada workspace puede apuntar a su propia instancia de Holded con su propia clave.

Nexo — integración bidireccional con Holded: jobs en queue, reintentos, resolución de conflictos por capa de autoridad

Observabilidad y operación en producción

Si no lo puedes operar, no lo tienes. Nexo corre en producción bajo nexo.kiwop.com con un stack sencillo: PHP 8.4 FPM con pool dedicado, MySQL/MariaDB 10.4, Node 20 vía nvm para build de frontend, y nginx por delante. En desarrollo, DDEV con réplica del stack en Docker. Paridad dev/prod acotada (PHP 8.2 local vs 8.4 producción).

La observabilidad diaria se apoya en tres fuentes. storage/logs/laravel.log es el log principal (errores, excepciones, warnings del engine de Brain cuando el contexto se va de tamaño). storage/logs/legacy-routes-*.log es el log dedicado de accesos a rutas legacy, rotado por día: si una ruta no aparece en dos semanas, es candidata a borrado. Y BrainProtocolRun es el audit trail estructurado de cada ejecución de agente, consultable por SQL.

El deploy sigue ./scripts/deploy.sh: artisan down, backup de DB, git pull, composer install --no-dev, build de frontend si cambiaron deps, artisan migrate --force, cachés, artisan up. Rollback: restaurar último mysqldump y git checkout <sha>. Ratio de deploys bajo, revisión humana por deploy.

Una pieza operativa interesante es el relay de Claude CLI (claude-relay.service). Algunos workspaces usan conexión IA por autenticación CLI (estilo OpenClaw) en lugar de API key, para consumir una suscripción Claude Max dedicada. El relay es un daemon PHP bajo systemd como usuario proves (nunca root) que traduce requests HTTP internas a llamadas al CLI claude -p. Patrón para desacoplar coste de IA del uso real de la plataforma.

Escalado y lecciones aprendidas

Conviene parar y contar qué ha dolido, qué haríamos distinto y qué añadiríamos si empezáramos hoy.

Lo que duele hoy. Los controllers de workspace requieren recordar la firma (Request $request, string $workspaceSlug, ...). Error humano cometido suficientes veces. Con perspectiva, lo habríamos resuelto con inyección de dependencia del CurrentWorkspace DTO en lugar de leer el slug del parámetro de ruta — ya lo registra EnsureWorkspaceContext, solo no lo usamos consistentemente como puerta única. Migrar los controllers a ese patrón es trabajo pendiente.

La convivencia Blade + Inertia ha durado más de lo planeado. El plan inicial era migrar toda la app en seis meses. A día de hoy, con el middleware guardián bloqueando writes desde hace más de un año, siguen existiendo vistas Blade de solo lectura que nadie ha priorizado migrar. No es un problema de seguridad pero sí de mantenimiento (dos stacks en paralelo) y de consistencia UX. Lección: la migración incremental con guardián funciona, pero necesita calendario explícito y un owner; sin dueño claro, la migración se cuelga.

Lo que no añadiríamos. Hemos resistido meter un framework de agentes externo (LangChain, CrewAI, Haystack) para Brain. Brain hace exactamente lo que necesitamos y un framework genérico habría multiplicado la superficie sin añadir capacidad funcional. Para un agente que vive dentro de una app Laravel con dominio propio, un motor a medida de pocas líneas bien entendidas pesa menos que un framework de miles de líneas que no controlas.

Lo que añadiríamos. Estamos empezando a pensar en WebMCP como protocolo externo para exponer Brain a agentes de terceros (Claude Code, Cursor, un PA del cliente). Si Brain ya ejecuta trabajo autónomo sobre el workspace con audit trail completo, exponerlo como MCP server permitiría a un agente externo ejecutar protocolos de Nexo como tools nativas. Es el patrón que analizamos en el artículo sobre MCP, WebMCP y A2A. Trabajo de los próximos meses.

Lo que no hemos publicado. No damos cifras concretas de workspaces activos, usuarios, tokens mensuales ni runs de protocolo al día. No por secretismo: cualquier número se queda desactualizado en tres meses y preferimos no dar métricas que se conviertan en "dato histórico incorrecto". Si aplica a una evaluación técnica previa a un proyecto, pregúntanos directamente.

Escala. Nexo no es SaaS masivo: decenas de workspaces B2B mid-market, cada uno con decenas a bajos cientos de usuarios. La arquitectura soporta holgadamente un orden de magnitud más sin rediseño; el cuello de botella real — en el orden de miles de workspaces — sería la sincronización con Holded (por rate limit de la API externa, no por Nexo) y la capa de cache.

Preguntas frecuentes

¿Por qué Inertia.js en vez de una SPA separada con API REST?

Porque Nexo es una aplicación autenticada interna, no un producto público masivo. Inertia da UX de SPA con deploy atómico, auth unificada, validación compartida y sesión server-side. Una SPA separada solo tiene sentido cuando necesitas múltiples frontends sobre la misma API o cuando frontend y backend viven en infraestructura distinta, ninguno de los cuales aplica. Mantenemos a la vez una REST API /api/v1 para integradores externos.

¿Compartir base de datos con columna `workspace_id` es seguro frente a schema por tenant?

Es seguro si aplicas disciplina: middleware que resuelva el workspace en cada request, trait WorkspaceScoped que aplique el filtro por defecto, y code review que bloquee desactivaciones del scope. El coste de schema por tenant (migraciones N veces, backups por tenant, analíticas federadas) no compensaba. Para cargas muy altas o requisitos regulatorios específicos (healthcare) el cálculo cambia.

¿Qué hace un `BrainProtocol` y cómo se configura?

Un protocolo es una secuencia de hasta 10 pasos tipados (connector_fetch, data_filter, ai_call, db_query, action, condition) que el motor ejecuta secuencialmente compartiendo un contexto mutable. Se configura desde el panel del workspace eligiendo el trigger (connector, schedule o manual), definiendo los pasos en orden y marcando cuáles son críticos. Cada run genera un BrainProtocolRun con audit trail completo.

¿Cuánto cuesta Brain en tokens al mes?

Depende del uso y del modelo. Los límites MAX_STEPS = 10 y MAX_CONTEXT_CHARS = 300000 son los cortafuegos duros, y cada ai_call incluye max_tokens propio (por defecto 2000). Para el perfil de uso actual, el coste es una línea menor de gastos operativos y se monitoriza por feature (chat, agent, extraction) a través del AiGateway.

¿Cómo gestionáis la integración con Holded si Holded cae?

Los jobs de sincronización están en queue con reintentos. Si Holded está caído, la sincronización se degrada sin romper Nexo: los hitos se siguen pudiendo crear y modificar, la creación en Holded queda encolada y se reintenta cuando la API responde. El estado visible es "pendiente de sincronización". No bloqueamos la operación del workspace por una caída del ERP.

¿Por qué habéis mantenido Blade antiguo durante más de un año?

Porque un big-bang rewrite de una app en producción con datos reales es un anti-patrón con historias de terror. El middleware legacy.guard bloquea writes desde el día uno, eliminando el riesgo de inconsistencias. Migrar vistas de solo lectura es trabajo sin fecha crítica, guiado por logs de uso real. La alternativa — parar features seis meses — habría sido mucho peor comercialmente.

¿Qué pasa si un desarrollador olvida `string $workspaceSlug` en un controller de workspace?

La petición llega al controller pero Laravel inyecta el slug en el siguiente parámetro ($id), el lookup falla y la request devuelve 404. Síntoma: 404 silencioso muy difícil de diagnosticar. La ruta index funciona, así que el bug solo aparece al abrir el primer detalle. Está documentado como regla absoluta y es lo primero que explicamos a cualquier desarrollador nuevo.

¿Se puede exponer Brain como MCP server para que un agente externo lo use?

Hoy no, está en roadmap. La pieza técnica existe (motor, audit trail, límites per-workspace) y el paso natural es envolverla en un MCP server que respete RBAC y toggles de módulos. Abre Nexo a ecosistemas de agentes externos sin abrir la base de datos. Si te interesa el caso de uso para tu propio SaaS, es parte de desarrollo de agentes IA y consultoría de IA.

Conclusión: construir un SaaS en 2026 no es elegir moda, es elegir disciplina

La conclusión honesta de convertir Nexo de una app Laravel 10 con Blade en una plataforma operativa completa con Laravel 12, Inertia + React y agentes IA es que la arquitectura técnica pesa mucho menos de lo que la gente cree al elegir stack, y mucho más de lo que la gente cree al mantenerlo. Nada de lo que contamos aquí es exótico: Laravel es maduro desde hace una década, Inertia es una pieza simple, el multi-tenancy con columna workspace_id es el patrón por defecto del ecosistema PHP, el RBAC con tablas roles + permissions + role_permissions está en cualquier curso de diseño de bases de datos. La diferencia entre un SaaS que escala y uno que se atranca no está en el nombre del framework — está en la disciplina con la que aplicas los middleware que bloquean accesos cruzados, en los límites explícitos que escribes a fuego cuando metes IA en el flujo, y en tu capacidad de convivir con código legacy sin convertirlo en una bomba de tiempo.

Nexo es el producto que mejor ilustra cómo trabajamos. Si tu empresa está planteándose construir un SaaS multi-tenant, si necesita introducir agentes IA de forma auditable en una aplicación que ya lleva años en producción, o si estás evaluando Nexo directamente como plataforma operativa, hablemos. Somos Kiwop, Agencia Digital especializada en Desarrollo de Software e Inteligencia Artificial aplicada para clientes globales en Europa y USA, y ofrecemos desarrollo a medida de agentes IA, consultoría de inteligencia artificial, integración de LLMs y RAG empresarial para contextos de producción. Puedes ver el caso de éxito público de Nexo o escribirnos directamente.

¿Quieres implementar inteligencia artificial en tu negocio?

Desarrollamos chatbots y soluciones de IA que automatizan procesos y mejoran la experiencia de tus clientes.

Descubre nuestro servicio de Chatbots IA

Consulta
técnica inicial.

IA, seguridad y rendimiento. Diagnóstico y propuesta cerrada por fases.

NDA disponible
Respuesta <24h
Propuesta por fases

Tu primera reunión es con un Arquitecto de Soluciones, no con un comercial.

Hablar con un arquitecto