Voltar ao Blog
Inteligência artificial

Nexo: anatomia técnica do nosso SaaS multi-tenant — Laravel 12 + React + Inertia.js + Brain (agentes IA)

Nexo — arquitetura de um SaaS multi-tenant com Laravel 12, React + Inertia.js e agentes IA Brain

Nexo: anatomia técnica do nosso SaaS multi-tenant — Laravel 12 + React + Inertia.js + Brain (agentes IA)

Pela equipa da Kiwop · Agência Digital especializada em Desenvolvimento de Software e Inteligência Artificial aplicada para clientes globais na Europa e EUA · Publicado a 19 de abril de 2026 · Última atualização: 19 de abril de 2026

TL;DR — Nexo é o SaaS operacional da Kiwop (HR, CRM, projetos, faturação, salários) sobre Laravel 12 + React + Inertia.js, com agentes IA próprios (Brain). Três decisões definem-no: hierarquia Organization → Workspace → Member com toggles de módulos por workspace, Inertia.js como ponte entre monolito e SPA, e coexistência controlada entre rotas legacy (Blade) e novas (Inertia) através de um middleware guardião que bloqueia escritas antigas.

Nexo — arquitetura de um SaaS multi-tenant com Laravel 12, React + Inertia.js e agentes IA Brain

Na Kiwop há anos que construímos software à medida para clientes da Europa e EUA, mas Nexo é diferente: é o produto que usamos todos os dias nós mesmos para operar a agência, e ao mesmo tempo o SaaS que oferecemos como caso de sucesso a empresas B2B mid-market que precisam de centralizar HR, CRM, projetos e faturação numa única plataforma. Quando constróis algo que a tua própria equipa usa diariamente, a arquitetura deixa de ser um exercício teórico: cada decisão paga-se ou cobra-se todas as segundas-feiras de manhã.

Este artigo entra no detalhe técnico. É a documentação do estado real do sistema a abril de 2026 — decisões, trade-offs e coisas que faríamos diferente. Encaixa no mesmo cluster que o nosso post sobre reconstruir um assistente pessoal com Claude Code e MCP, o guia para construir o teu PA com Claude Code, os padrões e antipadrões de agentes IA em produção e os protocolos MCP, WebMCP e A2A em 2026. Se esses posts tratam de agentes fora da app, Nexo é a demonstração de como metes agentes dentro de uma aplicação que já tem um domínio rico, dados reais e um tenancy sério.

O que é Nexo e para que o construímos

Nexo nasce de um problema recorrente em empresas B2B entre dez e duzentos trabalhadores: a operativa diária vive dispersa em meia dúzia de ferramentas desconectadas. Controlo horário numa app, CRM noutra, projetos em ClickUp, faturação num ERP, salários na gestoria, docs em Notion. Cada ferramenta funciona bem em separado, mas ninguém vê a operação como um todo, os dados de cliente duplicam-se entre sistemas, e cada relatório requer exportar CSVs de quatro sítios e cruzá-los à mão.

Na Kiwop sofríamos desse problema. Construímos Nexo primeiro para nós e depois convertêmo-lo em produto. A dia de hoje cobre cinco módulos:

  • HR: controlo horário (registos com geolocalização e rastreabilidade legal de quatro anos), gestão de férias e ausências, fichas de colaborador, equipas e feriados.
  • CRM: empresas, contactos, leads com pipeline, atividades comerciais e campanhas outbound.
  • Projects: projetos, tarefas com tempo registável, comentários, anexos e sessões de tempo.
  • Billing: contratos, hitos de faturação, avais de licitação, sincronização bidirecional com Holded ERP.
  • Payroll: salários com cálculo legal espanhol, suporte para vários acordos coletivos (TIC, Comercio Catalunya), exportação SILTRA.

Por cima disto tudo vivem duas peças que o separam de um SaaS operacional clássico: um subsistema de extração de contratos com IA (envias um PDF de contrato, a IA preenche o registo) e um motor de agentes IA próprio chamado Brain, que permite configurar protocolos multi-passo para que a plataforma execute trabalho autónomo sobre os dados do workspace.

Nexo corre em produção em nexo.kiwop.com com dezenas de workspaces ativos. Não são milhares: é um SaaS B2B mid-market, não uma app de consumo. E é apresentado no nosso site como caso de sucesso embora seja produto nosso, porque a história técnica é exatamente a mesma que contaríamos se o cliente fosse externo.

Porquê Laravel + Inertia.js + React (e não SPA pura)

A primeira decisão grande foi o esqueleto do frontend. Em 2025 o consenso por inércia era "SPA em React/Vue a consumir API REST ou GraphQL". Nós fomos por outra via: Laravel 12 no backend, React 18 + TypeScript no frontend, e Inertia.js como ponte. Não uma SPA separada. Um monolito moderno com vistas React renderizadas a partir de controladores Laravel.

Quando separas backend e frontend em dois repositórios ligados por API, ganhas uma coisa e perdes cinco. Ganhas liberdade para pôr outro frontend amanhã (uma app móvel, outra marca com a mesma API). Perdes: autenticação unificada, validação partilhada, SEO trivial, deploy atómico, estado de sessão server-side, e a capacidade de iterar uma feature num único commit. Para uma aplicação autenticada interna como Nexo, essas cinco coisas pesam muito mais do que a flexibilidade teórica.

Inertia resolve o encaixe. Os controladores Laravel, em vez de Blade ou JSON, devolvem Inertia::render('NombrePagina', $props). Isso renderiza um componente React concreto (resources/js/Pages/Workspace/Dashboard.tsx) com as props que o backend já calculou. Navegar é um fetch interno que troca componente e props sem recarregar: UX de SPA, verdade no servidor, autenticação por cookies e middleware Laravel de toda a vida.

O middleware HandleInertiaRequests partilha props globais (utilizador, workspace, papel, flags) a todas as páginas, pelo que nenhum componente pede esse estado em cada página nem montamos um Redux/Zustand para algo que o servidor já sabe. Deploy único: git pull, composer install, npm run build, limpeza de caches, sem janelas de incoerência entre API e cliente.

O custo é que ao abrir Nexo a integradores externos precisamos de API formal paralela. Montámo-la cedo: REST API `/api/v1` com Laravel Sanctum (tarefas, projetos, registos, docs, leads, billing, webhooks), a viver junto às rotas Inertia. Frontend próprio por Inertia, integradores por REST com tokens.

Nexo — stack e camadas: Laravel 12 + Inertia.js + React + MySQL + agentes Brain

O modelo multi-tenant: Organization → Workspace → Member

A segunda decisão estrutural é como se faz multi-tenancy. Duas escolas confrontadas: shared database com coluna de tenant contra schema per tenant. Escolhemos a primeira, com uma hierarquia de três níveis.

  • Organization: agrupação opcional para contas enterprise com vários workspaces sob a mesma marca.
  • Workspace: a unidade real de tenancy. Dados, configuração, módulos e identidade visual próprios. Identificado por slug na URL: /w/kiwop/dashboard.
  • WorkspaceMember: tabela pivot que liga utilizadores e workspaces com um papel (owner, admin, comercial, operations, member, viewer).

Escolhemos shared database com coluna workspace_id por três razões pragmáticas: evoluir o modelo sem migrar N esquemas, analíticas cruzadas de produto sem federar queries, e uma operação de backup/restore única. O custo é que um bug que omita um filtro por workspace_id pode, em teoria, cruzar dados entre tenants. Mitigamo-lo com duas peças.

A primeira é o middleware `EnsureWorkspaceContext`, que resolve o slug da URL ao modelo Workspace, verifica pertença do utilizador e, se o workspace pertence a uma organização, também a pertença a ela. Falha com 403 se algo não bater certo. Corre em todas as rotas sob /w/{workspaceSlug}/*.

A segunda é o trait `WorkspaceScoped` aplicado aos modelos Eloquent sensíveis (BrainProtocol, BrainProtocolRun, etc.), de modo que qualquer query aplica por defeito where workspace_id = ?. Desativá-lo requer fazê-lo explícito, e isso vê-se em code review.

Há um detalhe operacional que documentamos como regra absoluta: os controladores de workspace devem incluir string $workspaceSlug como segundo parâmetro (depois de Request $request) antes de qualquer outro. Laravel injeta parâmetros posicionalmente, e omitir o slug faz com que o seguinte parâmetro ($id) receba o slug e a request morra num 404 silencioso muito difícil de diagnosticar. A rota index funciona, o que faz com que o bug só apareça ao abrir o primeiro detalhe.

Nexo — hierarquia multi-tenant: Organization > Workspace > WorkspaceMember > User

A tabela seguinte resume as decisões arquitetónicas troncais que tomámos em Nexo, com a razão e o trade-off assumido. Se estás a desenhar um sistema parecido, esta tabela é provavelmente a parte do artigo que te convém copiar-e-colar num documento interno.

Module toggle system: ativar/desativar features por workspace

Cada workspace ativa ou desativa módulos de forma independente. O mecanismo: uma coluna JSON enabled_modules na tabela workspaces que mapeia módulo a booleano. A lista é fechada e vive em código como constante Workspace::MODULES (não deixamos que o utilizador crie módulos novos em tempo de execução). Inclui os blocos core (dashboard sempre ativo, fichaje, registros, solicitudes), analítica (rentabilidad, informes), trabalho (projects, tasks), vendas (leads, outbound, licitaciones), finanças (billing_contracts, billing, contabilidad), ferramentas (booking, decision_requests, chat), conhecimento (docs), settings (branding, notifications, settings), HR (employees, practicas, personal_documents, payroll) e módulos mais recentes como chatbot, pulse_surveys, proactive_agents, capacity_planner, project_pulses e meetings.

Dois pontos consultam o toggle: o método isModuleEnabled('leads') do modelo, e sobretudo o middleware `EnsureModuleEnabled` aplicado aos grupos de rotas:

O middleware verifica duas coisas por ordem: (1) se o módulo está ativado para o workspace, e (2) se o papel do utilizador nesse workspace pode aceder ao módulo. Perante qualquer falha, devolve 404 (não 403, para não filtrar a existência da rota). É uma pequena medida de security through obscurity que, combinada com as outras camadas, sobe a fasquia do reconhecimento para um atacante interno.

Porque não usamos feature flags clássicas (LaunchDarkly, Unleash, Flagsmith): os nossos toggles não são experimentos A/B nem roll-outs graduais, são configuração de produto por tenant. Um cliente sem módulo Payroll não o vai ter até o contratar. Meter um SaaS externo para gerir isso seria complexidade gratuita.

O módulo branding merece menção à parte: quando está ativo, o workspace pode personalizar a sua cor primária (CSS variable --workspace-primary), carregar logo e adaptar a identidade visual. Útil para white-label ou clientes enterprise que querem a marca interna. O CSS base mantém-se, só muda o acento.

RBAC: papéis + permissões sem bibliotecas externas

Para autorização, Nexo não usa Spatie Laravel-permission nem qualquer outra biblioteca. Temos sistema RBAC próprio: tabelas roles, permissions e role_permissions, com os papéis fixos (owner, admin, comercial, operations, member, viewer) e permissões granulares definidas em RbacSeeder. Três razões para não usar biblioteca.

Primeiro, o domínio de permissões está acoplado a conceitos próprios: workspace, organização, módulos, ações sobre entidades. Não é "utilizador pode fazer X", mas sim "este utilizador, neste workspace, com este papel, pode fazer X sobre Y". Com uma biblioteca genérica teríamos acabado por reescrever metade das camadas por cima.

Segundo, o Gate do Laravel já resolve a autorização. Definimos gates em AuthServiceProvider a apontar para policies (DocumentationPolicy::viewCategory) e usamos Gate::allows(...) ou @can(...) nas vistas. A única coisa que acrescentamos é a resolução do papel de workspace: $workspace->getUserRole($user) e $workspace->canRoleAccessModule($role, 'leads'), em cache em $request->attributes durante o ciclo de vida da request.

Terceiro, temos um mecanismo de simulação ("ver a app como outro utilizador", útil em suporte) que interage com o RBAC de forma específica: quando o admin simula, o bypass habitual "admin pode tudo" é desativado de propósito, e vês a app tal como a vê o utilizador simulado. Isso vive em EnsureModuleEnabled com $request->attributes->get('is_simulating', false). Por cima de uma biblioteca externa teria sido frágil.

Legacy coexistence: Blade antigo + Inertia novo em paralelo

Nexo tem uma história prévia. A versão original da aplicação estava construída em Laravel 10 com Blade + jQuery + Bootstrap. O frontend Inertia/React é posterior. A migração não se fez de uma vez: ainda fica código Blade vivo em produção. Essa coexistência é a realidade de qualquer aplicação real que leva vários anos em produção.

A pergunta é como evitas que essa coexistência se torne um problema. A nossa resposta é o middleware `LegacyRouteGuard`, registado como legacy.guard, aplicado a todas as rotas antigas. Faz três coisas:

  1. Loga qualquer acesso a rotas legacy em storage/logs/legacy-routes-*.log. Telemetria real de que rotas antigas continuam vivas, guia a priorização.
  2. Bloqueia POST/PUT/PATCH/DELETE com 409 Conflict e mensagem "Legacy interface is read-only". Nenhuma alteração de dados vai pela via antiga.
  3. Redireciona GETs para a equivalente nova quando há um mapeamento definido. O redirectMap associa rota legacy a rota workspace ('admin.horarios' => 'workspace.registros.index', etc.) e resolve parâmetros (workspaceSlug, articleId, contract) no momento.

A regra chave é "GETs abertos, writes fechados". Se bloqueias tudo, partes bookmarks e links antigos em correios. Se permites writes, tens dois caminhos para modificar os mesmos dados e não podes garantir que as validações, eventos e logs de auditoria do caminho novo disparem. A combinação "lê pelo caminho velho, escreve só pelo novo" permite não partir bookmarks enquanto migramos vistas uma a uma sem medo.

Há uma exceção: as rotas de simulação de admin (/admin/simulate-employee, /admin/stop-simulation, /admin/toggle-view-mode) não levam o guardião, porque são portas legítimas de suporte. O resto, sim.

É um padrão replicável: se tens uma app Laravel de 2019 com Blade e queres migrar para Inertia sem parar seis meses o negócio, acrescenta middleware guardião, bloqueia writes, loga acessos, mapeia redirects, e ataca a migração por frequência de acesso real (não por ordem alfabética nem por gosto estético).

Nexo — coexistência legacy Blade e novo Inertia com middleware guardião

Brain — o sistema de agentes IA de Nexo

Aqui entra a peça mais própria de Nexo: Brain, um motor de agentes IA multi-passo que vive dentro da aplicação. Não é um wrapper de LangChain nem um framework genérico. É um motor desenhado para executar automatismos sobre os dados de um workspace, com limites explícitos e rastreabilidade total.

O domínio são dois modelos: `BrainProtocol` (definição: nome, descrição, passos, configuração) e `BrainProtocolRun` (execução: contexto, resultados passo a passo, estado, tokens). Ambos vivem sob workspace_id com o trait WorkspaceScoped, pelo que nunca se cruzam entre tenants.

O motor `BrainProtocolEngine` processa passos sequencialmente com um contexto mutável em memória. Cada passo lê do contexto, executa a sua lógica e escreve sob uma output_key que os passos seguintes podem ler. Aplica dois limites duros escritos a fogo:

Um protocolo com mais de dez passos é truncado ao executar. Se o contexto acumulado ultrapassa 300.000 caracteres, logamos warning e antes de chamar o modelo truncamo-lo com um marcador [CONTEXTO TRUNCADO POR LIMITE DE TAMANHO]. Esses dois limites são a barreira contra o risco mais real de um agente: a explosão combinatória de contexto e custo quando itera sobre os seus próprios outputs.

Brain suporta seis tipos de passo:

Nexo — Brain Protocol Engine: seis tipos de passo executados sequencialmente sobre um contexto mutável

O passo ai_call é o que mais trabalho faz. Lê as chaves marcadas como input_keys, serializa-as para JSON sob cabeçalhos ### chave e acrescenta-as a uma secção ## DADOS DISPONÍVEIS concatenada ao prompt. Isso torna explícito o contrato prompt-contexto, evita que o modelo invente dados fora da secção, e facilita o debug: o contexto exato fica no audit trail.

O passo action é o que faz de Brain "um agente" e não um pipeline. Tipos suportados: create_task, create_proposal, save_memory, alert. Cada um resolve parâmetros com templates {{key.subkey}} contra o contexto: o título de uma tarefa pode ser {{ai_summary.content}} e o motor substitui-o antes de executar. Liga o raciocínio do modelo com a ação real sobre o domínio.

O passo condition é o corta-fogo. Com skip_remaining_if_false: true, se a condição não se cumpre, o resto do protocolo é saltado e os passos ficam marcados como skipped_remaining. Permite protocolos do tipo "se não há nada a fazer, não gastes tokens".

Exemplo: protocolo de onboarding de leads. (1) connector_fetch de leads novos das últimas 24h. (2) condition que aborta se a lista está vazia. (3) data_filter que fica com leads de score > 60, limite 20. (4) ai_call que gera um primeiro rascunho de correio. (5) action tipo create_proposal que deixa a proposta na caixa do comercial — não um correio enviado, porque as ações com efeito externo passam por aprovação humana.

Tudo fica registado em BrainProtocolRun: contexto final, resultados de cada passo com duração e estado, tokens, utilizador, fonte do trigger (manual, schedule, connector_update, agent_alert) e timestamps. Esse audit trail é a tabela pela qual um compliance officer pode auditar o que a IA fez sobre os dados do cliente. Sem ele, um SaaS com agentes não é auditável; com ele, é-o ao grão mais fino. Brain é a nossa materialização interna de muitos dos padrões que documentamos em agentes IA em produção: padrões e antipadrões para 2026.

Extração de contratos com IA + anti-alucinação

A outra peça importante de IA em Nexo é anterior a Brain e resolve um caso concreto: dar entrada a um contrato de faturação a partir do seu PDF. O trabalho manual de copiar nome de cliente, montante, datas, dados e-Fact, avais e hitos é peganhento e propenso a erros.

O subsistema `AiContractExtraction` resolve esse fluxo. Um endpoint POST /billing/contracts/ai-extract recebe o PDF (máximo 10MB), passa-o por PdfTextExtractor (baseado em smalot/pdfparser), e entrega-o ao extrator escolhido por AiExtractorFactory. A factory escolhe entre OpenAiContractExtractor (GPT-4o na sua versão estável mais recente, configurável por OPENAI_MODEL) ou o seu equivalente Anthropic consoante AI_PROVIDER. A interface ContractExtractorInterface garante que mudar de fornecedor é uma flag.

O prompt está otimizado para contratos em castelhano, com regras anti-alucinação explícitas. A resposta é JSON estruturado onde cada campo é um objeto com value, confidence (0-1) e evidence (citação literal do texto do PDF). Isso muda a UX radicalmente: o formulário preenche-se automaticamente apenas quando confidence >= 0.5, os campos preenchidos aparecem com borda azul clara e tooltip "Preenchido pela IA (confiança: 85%)", e um acordeão "o que encontrou a IA?" mostra a evidência literal. O humano nunca confia cegamente: revê, ajusta e guarda.

As regras anti-alucinação do system prompt são o núcleo de porque isto funciona. Se a IA não encontra um campo, o valor deve ser null e confidence deve ser 0. Se infere um valor por contexto (assumir 21% de IVA quando não está desagregado), deve baixar a confidence e acrescentar uma entrada a warnings. Os warnings mostram-se antes de guardar: "Não foi encontrado IVA desagregado, a assumir 21%", "Data de fim não especificada claramente". A combinação confidence + evidence + warnings + revisão humana obrigatória é o que faz com que uma extração com LLM seja operável num ambiente onde um erro te leva a emitir mal uma fatura.

Tudo fica na tabela ai_contract_extractions: quem carregou, quando, que ficheiro (com hash SHA256; não guardamos o texto completo, é importante para privacidade), que fornecedor, que modelo, tokens, custo estimado, resultado JSON, warnings, estado. Se cinco meses depois alguém perguntar "como chegaram estes dados ao contrato?", a resposta está na base de dados.

O motor geral por detrás — o que permite swap entre OpenAI e Anthropic com uma flag, o que gere limites de orçamento (AiBudgetGuard), o que resolve que ligação usar consoante workspace (AiConnectionResolver) — é o `AiGateway`, o mesmo que usam os ai_call de Brain. Centralizar a saída para LLMs dá três coisas: ponto único para rate limits e budgets, ponto único para logar tokens por feature (chat, agent, extraction), e ponto único para mudar de fornecedor sem tocar em código de negócio. Se amanhã testarmos um Sonnet 5 ou um GPT-6, a mudança vive numa única linha.

Esta abstração aplicamo-la também em projetos de cliente sob integração de LLMs e desenvolvimento de agentes IA. Centralizar a porta de saída para o modelo é um dos primeiros conselhos que damos a quem integra IA num produto em produção.

Integração com Holded ERP (bidirecional)

Nexo não substitui o ERP de faturação. Substitui o painel de gestão que olha para o ERP. Para faturação real continuamos a usar Holded e sincronizamos bidirecionalmente com ele. A integração vive sob app/Services/Holded/ com quatro peças: HoldedClient (cliente HTTP), HoldedSyncService (orquestrador), HoldedStatusMapper (mapeamento de estados) e SyncResult / BatchSyncResult (resultados tipados).

A sincronização segue o padrão clássico de integração com ERP externo: jobs em queue com retentativas, sincronização programada cada 15 minutos, e um Artisan command holded:sync-invoices --sync para forçar uma ronda manual. A direção bidirecional significa dois fluxos: de Nexo para Holded quando um hito é marcado como "emitido" (é criada a fatura) ou "pago" manualmente, e de Holded para Nexo quando um cliente paga uma fatura no ERP e a mudança de estado volta na ronda seguinte.

A gestão de conflitos é onde estas integrações se sujam. A nossa regra: "Holded ganha em estados financeiros, Nexo ganha em metadata comercial". O estado de pagamento é decidido pela Holded (sistema financeiro oficial). As etiquetas internas, notas e vínculos a projetos são decididos por Nexo. Essa divisão reduz os conflitos a um subconjunto manejável onde uma das duas partes é sempre autoritativa.

HOLDED_ENABLED ativa ou desativa a integração por ambiente. A configuração faz-se ao nível de workspace, não globalmente — cada workspace pode apontar para a sua própria instância de Holded com a sua própria chave.

Nexo — integração bidirecional com Holded: jobs em queue, retentativas, resolução de conflitos por camada de autoridade

Observabilidade e operação em produção

Se não o podes operar, não o tens. Nexo corre em produção sob nexo.kiwop.com com um stack simples: PHP 8.4 FPM com pool dedicado, MySQL/MariaDB 10.4, Node 20 via nvm para build de frontend, e nginx à frente. Em desenvolvimento, DDEV com réplica do stack em Docker. Paridade dev/prod delimitada (PHP 8.2 local vs 8.4 produção).

A observabilidade diária apoia-se em três fontes. storage/logs/laravel.log é o log principal (erros, exceções, warnings do engine de Brain quando o contexto foge do tamanho). storage/logs/legacy-routes-*.log é o log dedicado de acessos a rotas legacy, rodado por dia: se uma rota não aparece em duas semanas, é candidata a apagar. E BrainProtocolRun é o audit trail estruturado de cada execução de agente, consultável por SQL.

O deploy segue ./scripts/deploy.sh: artisan down, backup de DB, git pull, composer install --no-dev, build de frontend se mudaram deps, artisan migrate --force, caches, artisan up. Rollback: restaurar o último mysqldump e git checkout <sha>. Rácio de deploys baixo, revisão humana por deploy.

Uma peça operacional interessante é o relay de Claude CLI (claude-relay.service). Alguns workspaces usam ligação IA por autenticação CLI (estilo OpenClaw) em vez de API key, para consumir uma subscrição Claude Max dedicada. O relay é um daemon PHP sob systemd como utilizador proves (nunca root) que traduz requests HTTP internas a chamadas ao CLI claude -p. Padrão para desacoplar custo de IA do uso real da plataforma.

Escalado e lições aprendidas

Convém parar e contar o que doeu, o que faríamos diferente e o que acrescentaríamos se começássemos hoje.

O que dói hoje. Os controllers de workspace requerem lembrar a assinatura (Request $request, string $workspaceSlug, ...). Erro humano cometido suficientes vezes. Com perspetiva, teríamo-lo resolvido com injeção de dependência do CurrentWorkspace DTO em vez de ler o slug do parâmetro de rota — já o regista EnsureWorkspaceContext, só não o usamos consistentemente como porta única. Migrar os controllers para esse padrão é trabalho pendente.

A coexistência Blade + Inertia durou mais do que o planeado. O plano inicial era migrar toda a app em seis meses. A dia de hoje, com o middleware guardião a bloquear writes há mais de um ano, continuam a existir vistas Blade de só leitura que ninguém priorizou migrar. Não é um problema de segurança mas é de manutenção (dois stacks em paralelo) e de consistência UX. Lição: a migração incremental com guardião funciona, mas precisa de calendário explícito e um owner; sem dono claro, a migração pendura-se.

O que não acrescentaríamos. Resistimos a meter um framework de agentes externo (LangChain, CrewAI, Haystack) para Brain. Brain faz exatamente o que precisamos e um framework genérico teria multiplicado a superfície sem acrescentar capacidade funcional. Para um agente que vive dentro de uma app Laravel com domínio próprio, um motor à medida de poucas linhas bem entendidas pesa menos do que um framework de milhares de linhas que não controlas.

O que acrescentaríamos. Estamos a começar a pensar em WebMCP como protocolo externo para expor Brain a agentes de terceiros (Claude Code, Cursor, um PA do cliente). Se Brain já executa trabalho autónomo sobre o workspace com audit trail completo, expô-lo como MCP server permitiria a um agente externo executar protocolos de Nexo como tools nativas. É o padrão que analisamos em o artigo sobre MCP, WebMCP e A2A. Trabalho dos próximos meses.

O que não publicámos. Não damos números concretos de workspaces ativos, utilizadores, tokens mensais nem runs de protocolo por dia. Não por secretismo: qualquer número fica desatualizado em três meses e preferimos não dar métricas que se convertam em "dado histórico incorreto". Se aplicar a uma avaliação técnica prévia a um projeto, pergunta-nos diretamente.

Escala. Nexo não é SaaS massivo: dezenas de workspaces B2B mid-market, cada um com dezenas a baixas centenas de utilizadores. A arquitetura suporta folgadamente uma ordem de grandeza mais sem redesenho; o estrangulamento real — na ordem de milhares de workspaces — seria a sincronização com Holded (por rate limit da API externa, não por Nexo) e a camada de cache.

Perguntas frequentes

Porquê Inertia.js em vez de uma SPA separada com API REST?

Porque Nexo é uma aplicação autenticada interna, não um produto público massivo. Inertia dá UX de SPA com deploy atómico, auth unificada, validação partilhada e sessão server-side. Uma SPA separada só faz sentido quando precisas de múltiplos frontends sobre a mesma API ou quando frontend e backend vivem em infraestrutura distinta, nenhum dos quais se aplica. Mantemos ao mesmo tempo uma REST API /api/v1 para integradores externos.

Partilhar base de dados com coluna `workspace_id` é seguro face a schema por tenant?

É seguro se aplicas disciplina: middleware que resolva o workspace em cada request, trait WorkspaceScoped que aplique o filtro por defeito, e code review que bloqueie desativações do scope. O custo de schema por tenant (migrações N vezes, backups por tenant, analíticas federadas) não compensava. Para cargas muito altas ou requisitos regulatórios específicos (healthcare) o cálculo muda.

O que faz um `BrainProtocol` e como se configura?

Um protocolo é uma sequência de até 10 passos tipados (connector_fetch, data_filter, ai_call, db_query, action, condition) que o motor executa sequencialmente a partilhar um contexto mutável. Configura-se a partir do painel do workspace escolhendo o trigger (connector, schedule ou manual), definindo os passos em ordem e marcando quais são críticos. Cada run gera um BrainProtocolRun com audit trail completo.

Quanto custa Brain em tokens por mês?

Depende do uso e do modelo. Os limites MAX_STEPS = 10 e MAX_CONTEXT_CHARS = 300000 são os corta-fogos duros, e cada ai_call inclui max_tokens próprio (por defeito 2000). Para o perfil de uso atual, o custo é uma linha menor de despesas operacionais e monitoriza-se por feature (chat, agent, extraction) através do AiGateway.

Como gerem a integração com Holded se a Holded cai?

Os jobs de sincronização estão em queue com retentativas. Se Holded está caída, a sincronização degrada-se sem partir Nexo: os hitos continuam a poder criar-se e modificar-se, a criação em Holded fica em fila e retenta-se quando a API responde. O estado visível é "pendente de sincronização". Não bloqueamos a operação do workspace por uma queda do ERP.

Porque mantiveram Blade antigo durante mais de um ano?

Porque um big-bang rewrite de uma app em produção com dados reais é um antipadrão com histórias de terror. O middleware legacy.guard bloqueia writes desde o dia um, eliminando o risco de inconsistências. Migrar vistas de só leitura é trabalho sem data crítica, guiado por logs de uso real. A alternativa — parar features seis meses — teria sido muito pior comercialmente.

O que acontece se um programador esquece `string $workspaceSlug` num controller de workspace?

O pedido chega ao controller mas Laravel injeta o slug no parâmetro seguinte ($id), o lookup falha e a request devolve 404. Sintoma: 404 silencioso muito difícil de diagnosticar. A rota index funciona, pelo que o bug só aparece ao abrir o primeiro detalhe. Está documentado como regra absoluta e é a primeira coisa que explicamos a qualquer programador novo.

Pode expor-se Brain como MCP server para que um agente externo o use?

Hoje não, está em roadmap. A peça técnica existe (motor, audit trail, limites per-workspace) e o passo natural é envolvê-la num MCP server que respeite RBAC e toggles de módulos. Abre Nexo a ecossistemas de agentes externos sem abrir a base de dados. Se te interessa o caso de uso para o teu próprio SaaS, é parte de desenvolvimento de agentes IA e consultoria de IA.

Conclusão: construir um SaaS em 2026 não é escolher moda, é escolher disciplina

A conclusão honesta de converter Nexo de uma app Laravel 10 com Blade numa plataforma operacional completa com Laravel 12, Inertia + React e agentes IA é que a arquitetura técnica pesa muito menos do que as pessoas crêem ao escolher stack, e muito mais do que as pessoas crêem ao mantê-lo. Nada do que contamos aqui é exótico: Laravel é maduro há uma década, Inertia é uma peça simples, o multi-tenancy com coluna workspace_id é o padrão por defeito do ecossistema PHP, o RBAC com tabelas roles + permissions + role_permissions está em qualquer curso de desenho de bases de dados. A diferença entre um SaaS que escala e um que emperra não está no nome do framework — está na disciplina com a qual aplicas os middleware que bloqueiam acessos cruzados, nos limites explícitos que escreves a fogo quando metes IA no fluxo, e na tua capacidade de conviver com código legacy sem o converter numa bomba-relógio.

Nexo é o produto que melhor ilustra como trabalhamos. Se a tua empresa está a pensar em construir um SaaS multi-tenant, se precisa de introduzir agentes IA de forma auditável numa aplicação que já leva anos em produção, ou se estás a avaliar Nexo diretamente como plataforma operacional, falemos. Somos Kiwop, Agência Digital especializada em Desenvolvimento de Software e Inteligência Artificial aplicada para clientes globais na Europa e EUA, e oferecemos desenvolvimento à medida de agentes IA, consultoria de inteligência artificial, integração de LLMs e RAG empresarial para contextos de produção. Podes ver o caso de sucesso público de Nexo ou escrever-nos diretamente.

Quer implementar inteligência artificial no seu negócio?

Desenvolvemos chatbots e soluções de IA que automatizam processos e melhoram a experiência dos seus clientes.

Descubra o nosso serviço de Chatbots IA

Consulta
técnica inicial.

IA, segurança e desempenho. Diagnóstico com proposta faseada.

NDA disponível
Resposta <24h
Proposta faseada

A sua primeira reunião é com um Arquiteto de Soluções, não com um comercial.

Solicitar diagnóstico