Terug naar blog
Kunstmatige intelligentie

Nexo: technische anatomie van onze multi-tenant SaaS — Laravel 12 + React + Inertia.js + Brain (AI-agenten)

Nexo — architectuur van een multi-tenant SaaS met Laravel 12, React + Inertia.js en Brain AI-agenten

Nexo: technische anatomie van onze multi-tenant SaaS — Laravel 12 + React + Inertia.js + Brain (AI-agenten)

Door het team van Kiwop · Digital Agency gespecialiseerd in Softwareontwikkeling en toegepaste Kunstmatige Intelligentie voor wereldwijde klanten in Europa en de VS · Gepubliceerd op 19 april 2026 · Laatst bijgewerkt: 19 april 2026

TL;DR — Nexo is de operationele SaaS van Kiwop (HR, CRM, projecten, facturatie, loonadministratie) op Laravel 12 + React + Inertia.js, met eigen AI-agenten (Brain). Drie beslissingen definiëren hem: hiërarchie Organization → Workspace → Member met module-toggles per workspace, Inertia.js als brug tussen monoliet en SPA, en gecontroleerde coëxistentie tussen legacy-routes (Blade) en nieuwe (Inertia) via een guardian-middleware die oude writes blokkeert.

Nexo — architectuur van een multi-tenant SaaS met Laravel 12, React + Inertia.js en Brain AI-agenten

Bij Kiwop bouwen we al jaren software op maat voor klanten in Europa en de VS, maar Nexo is anders: het is het product dat we elke dag zelf gebruiken om het agency te laten draaien, en tegelijk de SaaS die we aanbieden als succescase aan B2B mid-market-bedrijven die HR, CRM, projecten en facturatie moeten centraliseren in één platform. Wanneer je iets bouwt dat je eigen team dagelijks gebruikt, stopt de architectuur een theoretische oefening te zijn: elke beslissing wordt elke maandagochtend betaald of geïnd.

Dit artikel gaat in op het technische detail. Het is de documentatie van de echte status van het systeem per april 2026 — beslissingen, trade-offs en dingen die we anders zouden doen. Het past in hetzelfde cluster als onze post over het herbouwen van een persoonlijke assistent met Claude Code en MCP, de gids om je PA te bouwen met Claude Code, patronen en antipatronen van AI-agenten in productie en de protocollen MCP, WebMCP en A2A in 2026. Als die posts agenten buiten de app behandelen, is Nexo de demonstratie van hoe je agenten binnen een applicatie stopt die al een rijk domein, echte data en serieuze tenancy heeft.

Wat Nexo is en waarom we hem hebben gebouwd

Nexo is ontstaan uit een terugkerend probleem in B2B-bedrijven van tien tot tweehonderd werknemers: de dagelijkse operatie zit verspreid over een half dozijn losse tools. Tijdregistratie in één app, CRM in een andere, projecten in ClickUp, facturatie in een ERP, loonadministratie bij de boekhouder, documenten in Notion. Elke tool werkt goed apart, maar niemand ziet de operatie als geheel, klantdata dupliceert tussen systemen, en elk rapport vereist CSV's exporteren uit vier plaatsen en handmatig kruisen.

Bij Kiwop leden we aan dat probleem. We bouwden Nexo eerst voor onszelf en maakten er daarna een product van. Vandaag dekt hij vijf modules:

  • HR: tijdregistratie (fichajes met geolocatie en juridische traceerbaarheid van vier jaar), beheer van vakantie en afwezigheden, werknemersfiches, teams en feestdagen.
  • CRM: bedrijven, contacten, leads met pipeline, commerciële activiteiten en outbound-campagnes.
  • Projects: projecten, taken met registreerbare tijd, opmerkingen, bijlagen en tijd-sessies.
  • Billing: contracten, facturatiemijlpalen, biedgaranties, bidirectionele synchronisatie met Holded ERP.
  • Payroll: loonadministratie met Spaanse juridische berekening, ondersteuning voor verschillende collectieve overeenkomsten (TIC, Comercio Catalunya), SILTRA-export.

Bovenop dat alles leven twee onderdelen die hem onderscheiden van een klassieke operationele SaaS: een subsysteem voor contractextractie met AI (je upload een contract-PDF, AI vult de aanmaak in) en een eigen AI-agent-engine genaamd Brain, waarmee multi-step protocollen kunnen worden geconfigureerd zodat het platform autonoom werk uitvoert op de workspace-data.

Nexo draait in productie op nexo.kiwop.com met tientallen actieve workspaces. Het zijn geen duizenden: het is een B2B mid-market SaaS, geen consumer-app. En hij wordt op onze website getoond als succescase hoewel het ons eigen product is, omdat het technische verhaal precies hetzelfde is als dat we zouden vertellen als de klant extern was.

Waarom Laravel + Inertia.js + React (en geen pure SPA)

De eerste grote beslissing was het skelet van de frontend. In 2025 was de consensus uit gewoonte "SPA in React/Vue die een REST- of GraphQL-API consumeert". Wij gingen een andere route: Laravel 12 in de backend, React 18 + TypeScript in de frontend, en Inertia.js als brug. Geen aparte SPA. Een moderne monoliet met React-views gerenderd vanuit Laravel-controllers.

Wanneer je backend en frontend scheidt in twee repositories verbonden via API, win je één ding en verlies je vijf. Je wint vrijheid om morgen een andere frontend te plaatsen (een mobiele app, een ander merk met dezelfde API). Je verliest: uniforme authenticatie, gedeelde validatie, triviale SEO, atomische deploy, server-side sessie-status, en de mogelijkheid om een feature in één commit te itereren. Voor een geauthenticeerde interne applicatie zoals Nexo wegen die vijf dingen veel zwaarder dan de theoretische flexibiliteit.

Inertia lost de passing op. De Laravel-controllers, in plaats van Blade of JSON, retourneren Inertia::render('PaginaNaam', $props). Dat rendert een concreet React-component (resources/js/Pages/Workspace/Dashboard.tsx) met de props die de backend al heeft berekend. Navigeren is een interne fetch die component en props uitwisselt zonder te herladen: SPA-UX, server-waarheid, cookie-authenticatie en Laravel-middleware van altijd.

De middleware HandleInertiaRequests deelt globale props (gebruiker, workspace, rol, flags) met alle pagina's, zodat geen component op elke pagina om die status vraagt en we geen Redux/Zustand opzetten voor iets wat de server al weet. Unieke deploy: git pull, composer install, npm run build, cache-opruimen, zonder incoherentievensters tussen API en client.

De kost is dat we bij het openstellen van Nexo voor externe integratoren een parallel formele API nodig hebben. We hebben het vroeg opgezet: REST API `/api/v1` met Laravel Sanctum (taken, projecten, records, docs, leads, billing, webhooks), naast de Inertia-routes. Eigen frontend via Inertia, integratoren via REST met tokens.

Nexo — stack en lagen: Laravel 12 + Inertia.js + React + MySQL + Brain-agenten

Het multi-tenant-model: Organization → Workspace → Member

De tweede structurele beslissing is hoe je multi-tenancy doet. Twee botsende scholen: shared database met tenant-kolom tegen schema per tenant. We kozen de eerste, met een hiërarchie van drie niveaus.

  • Organization: optionele groepering voor enterprise-accounts met meerdere workspaces onder hetzelfde merk.
  • Workspace: de echte eenheid van tenancy. Eigen data, configuratie, modules en visuele identiteit. Geïdentificeerd door slug in de URL: /w/kiwop/dashboard.
  • WorkspaceMember: pivot-tabel die gebruikers en workspaces verbindt met een rol (owner, admin, comercial, operations, member, viewer).

We kozen shared database met workspace_id-kolom om drie pragmatische redenen: het model ontwikkelen zonder N schema's te migreren, product-gekruiste analyses zonder queries te federeren, en één backup/restore-operatie. De kost is dat een bug die een workspace_id-filter oversla, in theorie data tussen tenants kan kruisen. We mitigeren dat met twee stukken.

Het eerste is de middleware `EnsureWorkspaceContext`, die de URL-slug oplost naar het Workspace-model, het lidmaatschap van de gebruiker verifieert en, als de workspace tot een organisatie behoort, ook het lidmaatschap daarin. Faalt met 403 als iets niet klopt. Draait op alle routes onder /w/{workspaceSlug}/*.

Het tweede is het trait `WorkspaceScoped` toegepast op gevoelige Eloquent-modellen (BrainProtocol, BrainProtocolRun, etc.), zodat elke query standaard where workspace_id = ? toepast. Uitschakelen vereist expliciete actie, en dat valt op in code review.

Er is een operationeel detail dat we als absolute regel documenteren: de workspace-controllers moeten string $workspaceSlug als tweede parameter bevatten (na Request $request) voor elke andere. Laravel injecteert parameters positioneel, en het weglaten van de slug zorgt ervoor dat de volgende parameter ($id) de slug ontvangt en de request sterft in een zeer moeilijk te diagnosticeren stille 404. De index-route werkt, wat ervoor zorgt dat de bug alleen verschijnt bij het openen van het eerste detail.

Nexo — multi-tenant hiërarchie: Organization > Workspace > WorkspaceMember > User

De volgende tabel vat de fundamentele architecturale beslissingen samen die we in Nexo hebben genomen, met de reden en de geaccepteerde trade-off. Als je een vergelijkbaar systeem ontwerpt, is deze tabel waarschijnlijk het deel van het artikel dat je in een intern document wilt kopiëren-plakken.

Module toggle system: features per workspace activeren/deactiveren

Elke workspace activeert of deactiveert modules onafhankelijk. Het mechanisme: een JSON-kolom enabled_modules in de workspaces-tabel die module op boolean mapt. De lijst is gesloten en leeft in code als constante Workspace::MODULES (we laten de gebruiker geen nieuwe modules aanmaken tijdens runtime). Bevat de core-blokken (dashboard altijd actief, fichaje, registros, solicitudes), analytics (rentabilidad, informes), werk (projects, tasks), verkoop (leads, outbound, licitaciones), financiën (billing_contracts, billing, contabilidad), tools (booking, decision_requests, chat), kennis (docs), settings (branding, notifications, settings), HR (employees, practicas, personal_documents, payroll) en meer recente modules als chatbot, pulse_surveys, proactive_agents, capacity_planner, project_pulses en meetings.

Twee punten raadplegen de toggle: de methode isModuleEnabled('leads') van het model, en vooral de middleware `EnsureModuleEnabled` toegepast op route-groepen:

De middleware controleert twee dingen in volgorde: (1) of de module geactiveerd is voor de workspace, en (2) of de rol van de gebruiker in die workspace toegang heeft tot de module. Bij elke fout retourneert het 404 (geen 403, om het bestaan van de route niet te lekken). Het is een kleine maatregel van security through obscurity die, gecombineerd met de andere lagen, de lat verhoogt voor herkenning door een interne aanvaller.

Waarom we geen klassieke feature flags gebruiken (LaunchDarkly, Unleash, Flagsmith): onze toggles zijn geen A/B-experimenten of graduele rollouts, het zijn productconfiguratie per tenant. Een klant zonder Payroll-module krijgt hem pas als hij hem contracteert. Er een externe SaaS voor plaatsen zou gratis complexiteit zijn.

De module branding verdient aparte vermelding: wanneer geactiveerd, kan de workspace zijn primaire kleur personaliseren (CSS-variabele --workspace-primary), een logo uploaden en de visuele identiteit aanpassen. Nuttig voor white-label of enterprise-klanten die hun interne merk willen. De basis-CSS wordt behouden, alleen de accent verandert.

RBAC: rollen + permissies zonder externe libraries

Voor autorisatie gebruikt Nexo geen Spatie Laravel-permission of andere library. We hebben een eigen RBAC-systeem: tabellen roles, permissions en role_permissions, met vaste rollen (owner, admin, comercial, operations, member, viewer) en granulaire permissies gedefinieerd in RbacSeeder. Drie redenen om geen library te gebruiken.

Ten eerste, het permissie-domein is gekoppeld aan eigen concepten: workspace, organisatie, modules, acties op entiteiten. Het is niet "gebruiker kan X doen", maar "deze gebruiker, in deze workspace, met deze rol, kan X doen op Y". Met een generieke library hadden we de helft van de lagen erbovenop herschreven.

Ten tweede, de Laravel Gate lost autorisatie al op. We definiëren gates in AuthServiceProvider die verwijzen naar policies (DocumentationPolicy::viewCategory) en gebruiken Gate::allows(...) of @can(...) in de views. Het enige dat we toevoegen is de resolutie van de workspace-rol: $workspace->getUserRole($user) en $workspace->canRoleAccessModule($role, 'leads'), gecached in $request->attributes tijdens de levenscyclus van de request.

Ten derde, we hebben een simulatie-mechanisme ("bekijk de app als een andere gebruiker", nuttig in support) dat specifiek interacteert met RBAC: wanneer de admin simuleert, wordt de gebruikelijke bypass "admin kan alles" expliciet uitgeschakeld, en zie je de app zoals de gesimuleerde gebruiker hem ziet. Dat leeft in EnsureModuleEnabled met $request->attributes->get('is_simulating', false). Bovenop een externe library zou het fragiel zijn geweest.

Legacy-coëxistentie: oude Blade + nieuwe Inertia parallel

Nexo heeft een eerdere geschiedenis. De originele versie van de applicatie was gebouwd in Laravel 10 met Blade + jQuery + Bootstrap. De Inertia/React-frontend is later. De migratie werd niet in één keer gedaan: er leeft nog steeds Blade-code in productie. Die coëxistentie is de realiteit van elke echte applicatie die al meerdere jaren in productie is.

De vraag is hoe je voorkomt dat die coëxistentie een probleem wordt. Ons antwoord is de middleware `LegacyRouteGuard`, geregistreerd als legacy.guard, toegepast op alle oude routes. Het doet drie dingen:

  1. Logt elke toegang tot legacy-routes in storage/logs/legacy-routes-*.log. Echte telemetrie van welke oude routes nog leven, leidt de prioritering.
  2. Blokkeert POST/PUT/PATCH/DELETE met 409 Conflict en bericht "Legacy interface is read-only". Geen datawijziging gaat via de oude weg.
  3. Redirect GETs naar het nieuwe equivalent wanneer er een mapping is gedefinieerd. De redirectMap koppelt legacy-route aan workspace-route ('admin.horarios' => 'workspace.registros.index', etc.) en lost parameters op (workspaceSlug, articleId, contract) op het moment.

De sleutelregel is "GETs open, writes gesloten". Als je alles blokkeert, breek je bookmarks en oude links in e-mails. Als je writes toestaat, heb je twee paden om dezelfde data te wijzigen en kun je niet garanderen dat de validaties, events en audit-logs van het nieuwe pad worden getriggerd. De combinatie "lees via de oude weg, schrijf alleen via de nieuwe" staat toe bookmarks niet te breken terwijl we views één voor één migreren zonder angst.

Er is een uitzondering: de admin-simulatieroutes (/admin/simulate-employee, /admin/stop-simulation, /admin/toggle-view-mode) dragen de guardian niet, omdat het legitieme support-deuren zijn. De rest wel.

Het is een repliceerbaar patroon: als je een Laravel-app uit 2019 hebt met Blade en wilt migreren naar Inertia zonder zes maanden business te stoppen, voeg een guardian-middleware toe, blokkeer writes, log toegangen, map redirects, en pak de migratie aan volgens werkelijke toegangsfrequentie (niet alfabetische volgorde of esthetische smaak).

Nexo — legacy-coëxistentie Blade en nieuw Inertia met guardian-middleware

Brain — het AI-agent-systeem van Nexo

Hier komt het meest eigen onderdeel van Nexo binnen: Brain, een multi-step AI-agent-engine die in de applicatie leeft. Het is geen LangChain-wrapper of generiek framework. Het is een engine ontworpen om automatiseringen uit te voeren op de data van een workspace, met expliciete limieten en volledige traceerbaarheid.

Het domein bestaat uit twee modellen: `BrainProtocol` (definitie: naam, beschrijving, stappen, configuratie) en `BrainProtocolRun` (uitvoering: context, stapsgewijze resultaten, status, tokens). Beide leven onder workspace_id met het trait WorkspaceScoped, dus ze kruisen nooit tussen tenants.

De engine `BrainProtocolEngine` verwerkt stappen sequentieel met een muteerbare context in geheugen. Elke stap leest uit de context, voert zijn logica uit en schrijft onder een output_key die de volgende stappen kunnen lezen. Past twee harde, vurig geschreven limieten toe:

Een protocol met meer dan tien stappen wordt afgekapt bij uitvoering. Als de opgebouwde context 300.000 karakters overschrijdt, loggen we een warning en kappen we hem af voordat we het model aanroepen met een marker [CONTEXTO TRUNCADO POR LÍMITE DE TAMAÑO]. Die twee limieten zijn de barrière tegen het meest echte risico van een agent: de combinatorische explosie van context en kosten wanneer hij over zijn eigen outputs itereert.

Brain ondersteunt zes step types:

Nexo — Brain Protocol Engine: zes step types sequentieel uitgevoerd op een muteerbare context

Het ai_call-step doet het meeste werk. Het leest de als input_keys gemarkeerde sleutels, serialiseert ze naar JSON onder headers ### sleutel en voegt ze toe aan een sectie ## DATOS DISPONIBLES geconcateneerd aan de prompt. Dat maakt het prompt-context-contract expliciet, voorkomt dat het model data buiten de sectie verzint, en vergemakkelijkt debug: de exacte context blijft in het audit trail.

Het action-step is wat Brain tot "een agent" maakt en niet een pipeline. Ondersteunde types: create_task, create_proposal, save_memory, alert. Elk lost parameters op met templates {{key.subkey}} tegen de context: de titel van een taak kan {{ai_summary.content}} zijn en de engine vervangt het vóór uitvoering. Verbindt de redenering van het model met de echte actie op het domein.

Het condition-step is de brandmuur. Met skip_remaining_if_false: true, als de conditie niet wordt voldaan, wordt de rest van het protocol overgeslagen en worden de stappen gemarkeerd als skipped_remaining. Staat protocollen toe van het type "als er niets te doen is, verbrand geen tokens".

Voorbeeld: lead-onboarding-protocol. (1) connector_fetch van nieuwe leads van de laatste 24u. (2) condition die afbreekt als de lijst leeg is. (3) data_filter die leads met score > 60 behoudt, limiet 20. (4) ai_call die een eerste mail-concept genereert. (5) action van het type create_proposal die het voorstel in de commerciële inbox achterlaat — geen verzonden mail, omdat acties met extern effect door menselijke goedkeuring gaan.

Alles wordt geregistreerd in BrainProtocolRun: uiteindelijke context, resultaten van elke stap met duur en status, tokens, gebruiker, triggerbron (manueel, schedule, connector_update, agent_alert) en timestamps. Dat audit trail is de tabel waarmee een compliance officer kan auditeren wat de AI heeft gedaan op de klantdata. Zonder is een SaaS met agenten niet auditeerbaar; ermee, is hij dat tot op het fijnste detail. Brain is onze interne materialisering van veel van de patronen die we documenteren in AI-agenten in productie: patronen en antipatronen voor 2026.

Contractextractie met AI + anti-hallucinatie

Het andere belangrijke AI-onderdeel in Nexo is ouder dan Brain en lost een concreet geval op: een facturatiecontract aanmaken vanuit zijn PDF. Het handwerk van klantnaam, bedrag, datums, e-Fact-gegevens, garanties en mijlpalen kopiëren is plakkerig en foutgevoelig.

Het subsysteem `AiContractExtraction` lost die flow op. Een endpoint POST /billing/contracts/ai-extract ontvangt de PDF (maximaal 10MB), haalt hem door PdfTextExtractor (gebaseerd op smalot/pdfparser), en levert hem aan de extractor gekozen door AiExtractorFactory. De factory kiest tussen OpenAiContractExtractor (GPT-4o in zijn meest recente stabiele versie, configureerbaar via OPENAI_MODEL) of zijn Anthropic-equivalent volgens AI_PROVIDER. De interface ContractExtractorInterface garandeert dat van provider wisselen een flag is.

De prompt is geoptimaliseerd voor contracten in het Spaans, met expliciete anti-hallucinatie-regels. Het antwoord is gestructureerde JSON waarin elk veld een object is met value, confidence (0-1) en evidence (letterlijk citaat uit de PDF-tekst). Dat verandert de UX radicaal: het formulier wordt alleen automatisch ingevuld wanneer confidence >= 0.5, de ingevulde velden verschijnen met een lichtblauwe rand en tooltip "Ingevuld door AI (vertrouwen: 85%)", en een accordeon "wat vond de AI?" toont het letterlijke bewijs. De mens vertrouwt nooit blindelings: reviewt, past aan en slaat op.

De anti-hallucinatie-regels van de system prompt zijn de kern van waarom dit werkt. Als de AI een veld niet vindt, moet de waarde null zijn en confidence 0. Als hij een waarde afleidt uit context (21% BTW aannemen wanneer het niet gespecificeerd is), moet hij de confidence verlagen en een entry toevoegen aan warnings. De warnings worden getoond vóór opslaan: "Geen gespecificeerde BTW gevonden, 21% aangenomen", "Einddatum niet duidelijk gespecificeerd". De combinatie confidence + evidence + warnings + verplichte menselijke review is wat een LLM-extractie bedrijfsklaar maakt in een omgeving waar een fout je een foute factuur laat uitsturen.

Alles blijft in de tabel ai_contract_extractions: wie heeft geüpload, wanneer, welk bestand (met SHA256-hash; we bewaren de volledige tekst niet, het is belangrijk voor privacy), welke provider, welk model, tokens, geschatte kosten, JSON-resultaat, warnings, status. Als iemand vijf maanden later vraagt "hoe kwamen deze gegevens in het contract?", ligt het antwoord in de database.

De algemene engine erachter — die de swap tussen OpenAI en Anthropic met een flag mogelijk maakt, die budgetlimieten beheert (AiBudgetGuard), die oplost welke verbinding te gebruiken per workspace (AiConnectionResolver) — is de `AiGateway`, dezelfde die de ai_call van Brain gebruiken. Het centraliseren van de uitgang naar LLM's geeft drie dingen: enkel punt voor rate limits en budgets, enkel punt om tokens per feature (chat, agent, extraction) te loggen, en enkel punt om provider te wisselen zonder business-code aan te raken. Als we morgen een Sonnet 5 of een GPT-6 testen, leeft de wijziging in één regel.

Deze abstractie passen we ook toe in klantprojecten onder LLM-integratie en ontwikkeling van AI-agenten. Het centraliseren van de uitgangspoort naar het model is een van de eerste adviezen die we geven aan wie AI integreert in een product in productie.

Integratie met Holded ERP (bidirectioneel)

Nexo vervangt niet de facturatie-ERP. Het vervangt het beheerspaneel dat naar de ERP kijkt. Voor echte facturatie blijven we Holded gebruiken en synchroniseren we bidirectioneel met hem. De integratie leeft onder app/Services/Holded/ met vier onderdelen: HoldedClient (HTTP-client), HoldedSyncService (orkestrator), HoldedStatusMapper (statusmapping) en SyncResult / BatchSyncResult (getypeerde resultaten).

De synchronisatie volgt het klassieke patroon van integratie met externe ERP: queue-jobs met retries, geplande synchronisatie elke 15 minuten, en een Artisan-commando holded:sync-invoices --sync om een manuele ronde te forceren. De bidirectionele richting betekent twee flows: van Nexo naar Holded wanneer een mijlpaal wordt gemarkeerd als "uitgegeven" (de factuur wordt aangemaakt) of "handmatig betaald", en van Holded naar Nexo wanneer een klant een factuur in de ERP betaalt en de statuswijziging terugkomt in de volgende ronde.

Conflictbeheer is waar deze integraties vies worden. Onze regel: "Holded wint in financiële statussen, Nexo wint in commerciële metadata". De betalingsstatus bepaalt Holded (officieel financieel systeem). Interne labels, notities en projectverbindingen bepaalt Nexo. Die verdeling reduceert conflicten tot een beheersbare subset waarin een van de twee partijen altijd gezaghebbend is.

HOLDED_ENABLED activeert of deactiveert de integratie per omgeving. De configuratie gebeurt op workspace-niveau, niet globaal — elke workspace kan naar zijn eigen Holded-instantie wijzen met zijn eigen sleutel.

Nexo — bidirectionele integratie met Holded: queue-jobs, retries, conflictresolutie per autoriteitslaag

Observability en operatie in productie

Als je het niet kunt opereren, heb je het niet. Nexo draait in productie onder nexo.kiwop.com met een eenvoudige stack: PHP 8.4 FPM met toegewijde pool, MySQL/MariaDB 10.4, Node 20 via nvm voor frontend-build, en nginx ervoor. In ontwikkeling, DDEV met replica van de stack in Docker. Afgebakende dev/prod-pariteit (PHP 8.2 lokaal vs 8.4 productie).

De dagelijkse observability steunt op drie bronnen. storage/logs/laravel.log is het hoofdlog (fouten, excepties, Brain-engine-warnings wanneer de context te groot wordt). storage/logs/legacy-routes-*.log is het toegewijde log van legacy-routetoegangen, dagelijks geroteerd: als een route twee weken niet verschijnt, is hij kandidaat voor verwijdering. En BrainProtocolRun is het gestructureerde audit trail van elke agent-uitvoering, raadpleegbaar via SQL.

Deploy volgt ./scripts/deploy.sh: artisan down, DB-backup, git pull, composer install --no-dev, frontend-build als deps veranderden, artisan migrate --force, caches, artisan up. Rollback: laatste mysqldump restoren en git checkout <sha>. Lage deploy-ratio, menselijke review per deploy.

Een interessant operationeel onderdeel is de Claude CLI relay (claude-relay.service). Sommige workspaces gebruiken AI-verbinding via CLI-authenticatie (stijl OpenClaw) in plaats van API-sleutel, om een toegewijd Claude Max-abonnement te consumeren. De relay is een PHP-daemon onder systemd als gebruiker proves (nooit root) die interne HTTP-requests vertaalt naar oproepen aan de CLI claude -p. Patroon om de AI-kosten te ontkoppelen van het werkelijke gebruik van het platform.

Schaalvergroting en geleerde lessen

Het is nuttig te stoppen en te vertellen wat pijn heeft gedaan, wat we anders zouden doen en wat we zouden toevoegen als we vandaag zouden beginnen.

Wat vandaag pijn doet. De workspace-controllers vereisen dat je de signatuur (Request $request, string $workspaceSlug, ...) onthoudt. Menselijke fout vaak genoeg gemaakt. Met perspectief hadden we het opgelost met dependency injection van de CurrentWorkspace DTO in plaats van de slug uit de route-parameter te lezen — EnsureWorkspaceContext registreert hem al, we gebruiken hem alleen niet consistent als enkele poort. Het migreren van de controllers naar dat patroon is openstaand werk.

De Blade + Inertia-coëxistentie heeft langer geduurd dan gepland. Het initiële plan was de hele app in zes maanden te migreren. Vandaag, met de guardian-middleware die al meer dan een jaar writes blokkeert, blijven er nog Blade-views bestaan die alleen-lezen zijn en die niemand heeft geprioriteerd te migreren. Het is geen beveiligingsprobleem maar wel een onderhoudsprobleem (twee parallelle stacks) en UX-consistentie. Les: incrementele migratie met guardian werkt, maar heeft een expliciet schema en een eigenaar nodig; zonder duidelijke eigenaar blijft de migratie hangen.

Wat we niet zouden toevoegen. We hebben weerstand geboden om een extern agent-framework (LangChain, CrewAI, Haystack) voor Brain in te voeren. Brain doet precies wat we nodig hebben en een generiek framework had het oppervlak vermenigvuldigd zonder functionele capaciteit toe te voegen. Voor een agent die leeft binnen een Laravel-app met eigen domein, weegt een aangepaste engine van weinig goed begrepen regels minder dan een framework van duizenden regels dat je niet controleert.

Wat we zouden toevoegen. We beginnen na te denken over WebMCP als extern protocol om Brain bloot te stellen aan agenten van derden (Claude Code, Cursor, een klant-PA). Als Brain al autonoom werk uitvoert op de workspace met volledig audit trail, zou het blootstellen als MCP server een externe agent in staat stellen Nexo-protocollen uit te voeren als native tools. Het is het patroon dat we analyseren in het artikel over MCP, WebMCP en A2A. Werk voor de komende maanden.

Wat we niet hebben gepubliceerd. We geven geen concrete cijfers van actieve workspaces, gebruikers, maandelijkse tokens of protocol-runs per dag. Niet uit geheimhouding: elk getal veroudert in drie maanden en we geven liever geen metrics die "historisch onjuist gegeven" worden. Als het van toepassing is op een technische evaluatie voorafgaand aan een project, vraag het ons direct.

Schaal. Nexo is geen massa-SaaS: tientallen B2B mid-market workspaces, elk met tientallen tot lage honderden gebruikers. De architectuur ondersteunt ruim een orde van grootte meer zonder herontwerp; het echte knelpunt — in de orde van duizenden workspaces — zou de synchronisatie met Holded zijn (vanwege externe API-rate limit, niet vanwege Nexo) en de cache-laag.

Veelgestelde vragen

Waarom Inertia.js in plaats van een aparte SPA met REST API?

Omdat Nexo een geauthentiseerde interne applicatie is, geen massaal publiek product. Inertia geeft SPA-UX met atomische deploy, uniforme auth, gedeelde validatie en server-side sessie. Een aparte SPA is alleen zinvol wanneer je meerdere frontends over dezelfde API nodig hebt of wanneer frontend en backend in verschillende infrastructuur leven, geen van beide van toepassing. We onderhouden tegelijkertijd een REST API /api/v1 voor externe integratoren.

Is database delen met `workspace_id`-kolom veilig ten opzichte van schema per tenant?

Het is veilig als je discipline toepast: middleware die de workspace oplost in elke request, WorkspaceScoped-trait die het filter standaard toepast, en code review die scope-deactivaties blokkeert. De kost van schema per tenant (migraties N keer, backups per tenant, gefedereerde analyses) was het niet waard. Voor zeer hoge belastingen of specifieke regelgevingseisen (healthcare) verandert de berekening.

Wat doet een `BrainProtocol` en hoe wordt hij geconfigureerd?

Een protocol is een sequentie van maximaal 10 getypeerde stappen (connector_fetch, data_filter, ai_call, db_query, action, condition) die de engine sequentieel uitvoert met een gedeelde muteerbare context. Hij wordt geconfigureerd vanuit het workspace-paneel door de trigger te kiezen (connector, schedule of manueel), de stappen in volgorde te definiëren en te markeren welke kritiek zijn. Elke run genereert een BrainProtocolRun met volledig audit trail.

Hoeveel kost Brain aan tokens per maand?

Afhankelijk van het gebruik en het model. De limieten MAX_STEPS = 10 en MAX_CONTEXT_CHARS = 300000 zijn de harde brandmuren, en elke ai_call bevat zijn eigen max_tokens (standaard 2000). Voor het huidige gebruiksprofiel is de kost een kleine lijn in de operationele uitgaven en wordt hij per feature (chat, agent, extraction) gemonitord via de AiGateway.

Hoe beheren jullie de integratie met Holded als Holded valt?

De synchronisatie-jobs staan in queue met retries. Als Holded valt, degradeert de synchronisatie zonder Nexo te breken: de mijlpalen kunnen nog steeds worden aangemaakt en gewijzigd, de aanmaak in Holded wordt in de wachtrij gezet en opnieuw geprobeerd wanneer de API reageert. De zichtbare status is "wacht op synchronisatie". We blokkeren de workspace-operatie niet door een ERP-storing.

Waarom hebben jullie oude Blade meer dan een jaar behouden?

Omdat een big-bang rewrite van een app in productie met echte data een antipatroon is met horrorverhalen. De middleware legacy.guard blokkeert writes vanaf dag één, waardoor het risico op inconsistenties wordt geëlimineerd. Alleen-lezen-views migreren is werk zonder kritieke deadline, geleid door echte gebruiks-logs. Het alternatief — zes maanden features stoppen — zou commercieel veel erger zijn geweest.

Wat gebeurt er als een ontwikkelaar `string $workspaceSlug` vergeet in een workspace-controller?

De request komt aan bij de controller maar Laravel injecteert de slug in de volgende parameter ($id), de lookup faalt en de request retourneert 404. Symptoom: zeer moeilijk te diagnosticeren stille 404. De index-route werkt, dus de bug verschijnt alleen bij het openen van het eerste detail. Gedocumenteerd als absolute regel en het eerste dat we uitleggen aan elke nieuwe ontwikkelaar.

Kan Brain worden blootgesteld als MCP server zodat een externe agent hem gebruikt?

Vandaag niet, het staat op de roadmap. Het technische onderdeel bestaat (engine, audit trail, limieten per workspace) en de natuurlijke stap is het verpakken in een MCP server die RBAC en module-toggles respecteert. Opent Nexo voor ecosystemen van externe agenten zonder de database te openen. Als je geïnteresseerd bent in de use case voor je eigen SaaS, maakt het deel uit van ontwikkeling van AI-agenten en AI-consultancy.

Conclusie: een SaaS bouwen in 2026 is geen mode kiezen, het is discipline kiezen

De eerlijke conclusie van Nexo converteren van een Laravel 10-app met Blade naar een volledig operationeel platform met Laravel 12, Inertia + React en AI-agenten is dat de technische architectuur veel minder weegt dan mensen denken bij het kiezen van de stack, en veel meer dan mensen denken bij het onderhouden ervan. Niets van wat we hier vertellen is exotisch: Laravel is al een decennium volwassen, Inertia is een eenvoudig onderdeel, multi-tenancy met workspace_id-kolom is het standaardpatroon van het PHP-ecosysteem, RBAC met roles + permissions + role_permissions-tabellen staat in elke database-designcursus. Het verschil tussen een schalende SaaS en een die vastloopt zit niet in de naam van het framework — het zit in de discipline waarmee je de middleware toepast die kruistoegangen blokkeert, in de expliciete limieten die je vurig schrijft wanneer je AI in de flow stopt, en in je vermogen om met legacy-code te leven zonder er een tijdbom van te maken.

Nexo is het product dat het beste illustreert hoe we werken. Als je bedrijf overweegt een multi-tenant SaaS te bouwen, als het AI-agenten op een auditeerbare manier moet introduceren in een applicatie die al jaren in productie is, of als je Nexo rechtstreeks evalueert als operationeel platform, laten we praten. We zijn Kiwop, Digital Agency gespecialiseerd in Softwareontwikkeling en toegepaste Kunstmatige Intelligentie voor wereldwijde klanten in Europa en de VS, en we bieden ontwikkeling van AI-agenten op maat, AI-consultancy, LLM-integratie en enterprise RAG voor productiecontexten. Je kunt de publieke succescase van Nexo bekijken of ons direct schrijven.

Technisch
intakegesprek.

AI, beveiliging en prestaties. Diagnose met gefaseerd voorstel.

NDA beschikbaar
Antwoord <24u
Gefaseerd voorstel

Je eerste gesprek is met een Solutions Architect, niet met een verkoper.

Diagnose aanvragen