Nexo: technische Anatomie unseres multi-tenant SaaS — Laravel 12 + React + Inertia.js + Brain (KI-Agenten)
Vom Kiwop-Team · Digitalagentur spezialisiert auf Softwareentwicklung und angewandte Künstliche Intelligenz für globale Kunden in Europa und den USA · Veröffentlicht am 19. April 2026 · Zuletzt aktualisiert: 19. April 2026
TL;DR — Nexo ist der operative SaaS von Kiwop (HR, CRM, Projekte, Fakturierung, Gehaltsabrechnung) auf Laravel 12 + React + Inertia.js, mit eigenen KI-Agenten (Brain). Drei Entscheidungen definieren ihn: Hierarchie Organization → Workspace → Member mit Modul-Toggles pro Workspace, Inertia.js als Brücke zwischen Monolith und SPA und kontrollierte Koexistenz zwischen Legacy-Routen (Blade) und neuen (Inertia) über eine Guardian-Middleware, die alte Schreibvorgänge blockiert.

Bei Kiwop bauen wir seit Jahren maßgeschneiderte Software für Kunden in Europa und den USA, aber Nexo ist anders: Es ist das Produkt, das wir selbst jeden Tag nutzen, um die Agentur zu betreiben, und gleichzeitig der SaaS, den wir als Erfolgsgeschichte für B2B-Mid-Market-Unternehmen anbieten, die HR, CRM, Projekte und Fakturierung auf einer einzigen Plattform zentralisieren müssen. Wenn Du etwas baust, das Dein eigenes Team täglich nutzt, hört die Architektur auf, eine theoretische Übung zu sein: Jede Entscheidung wird jeden Montagmorgen bezahlt oder kassiert.
Dieser Artikel geht ins technische Detail. Er ist die Dokumentation des tatsächlichen Systemzustands zum April 2026 — Entscheidungen, Trade-offs und Dinge, die wir anders machen würden. Er passt in dasselbe Cluster wie unser Post über den Wiederaufbau eines persönlichen Assistenten mit Claude Code und MCP, den Leitfaden zum Bau Deines PA mit Claude Code, die Muster und Antimuster von KI-Agenten in Produktion und die Protokolle MCP, WebMCP und A2A in 2026. Wenn diese Posts Agenten außerhalb der App behandeln, ist Nexo die Demonstration, wie Du Agenten innerhalb einer Anwendung einfügst, die bereits eine reiche Domäne, echte Daten und eine ernsthafte Tenancy hat.
Was Nexo ist und wofür wir es gebaut haben
Nexo entsteht aus einem wiederkehrenden Problem in B2B-Unternehmen zwischen zehn und zweihundert Mitarbeitern: Die tägliche Operation lebt verteilt auf einem halben Dutzend getrennter Werkzeuge. Zeiterfassung in einer App, CRM in einer anderen, Projekte in ClickUp, Fakturierung in einem ERP, Gehaltsabrechnung beim Steuerberater, Docs in Notion. Jedes Werkzeug funktioniert einzeln gut, aber niemand sieht die Operation als Ganzes, Kundendaten werden zwischen Systemen dupliziert, und jeder Bericht erfordert den Export von CSVs aus vier Orten und deren manuelle Kreuzung.
Bei Kiwop litten wir unter diesem Problem. Wir haben Nexo zunächst für uns gebaut und es dann in ein Produkt verwandelt. Heute deckt es fünf Module ab:
- HR: Zeiterfassung (Stempel mit Geolokalisierung und rechtlicher Nachverfolgbarkeit über vier Jahre), Urlaubs- und Abwesenheitsverwaltung, Mitarbeiterakten, Teams und Feiertage.
- CRM: Unternehmen, Kontakte, Leads mit Pipeline, Vertriebsaktivitäten und Outbound-Kampagnen.
- Projects: Projekte, Aufgaben mit erfassbarer Zeit, Kommentare, Anhänge und Zeit-Sessions.
- Billing: Verträge, Abrechnungsmeilensteine, Ausschreibungsbürgschaften, bidirektionale Synchronisation mit Holded ERP.
- Payroll: Gehaltsabrechnungen mit spanischer Rechtsberechnung, Unterstützung für mehrere Tarifverträge (TIC, Comercio Catalunya), SILTRA-Export.
Darüber leben zwei Stücke, die ihn von einem klassischen operativen SaaS trennen: ein Subsystem zur KI-Vertragsextraktion (Du lädst ein Vertrags-PDF hoch, die KI füllt die Anlage aus) und ein eigener KI-Agenten-Motor namens Brain, der es ermöglicht, Multi-Step-Protokolle zu konfigurieren, damit die Plattform autonome Arbeit über den Daten des Workspace ausführt.
Nexo läuft in Produktion auf nexo.kiwop.com mit Dutzenden aktiven Workspaces. Es sind keine Tausende: Es ist ein B2B-Mid-Market-SaaS, keine Consumer-App. Und er wird auf unserer Website als Erfolgsgeschichte gezeigt, obwohl er unser Produkt ist, weil die technische Geschichte genau dieselbe ist, die wir erzählen würden, wenn der Kunde extern wäre.
Warum Laravel + Inertia.js + React (und keine reine SPA)
Die erste große Entscheidung war das Gerüst des Frontends. 2025 war der Konsens aus Trägheit "SPA in React/Vue, die eine REST- oder GraphQL-API konsumiert". Wir sind einen anderen Weg gegangen: Laravel 12 im Backend, React 18 + TypeScript im Frontend und Inertia.js als Brücke. Keine getrennte SPA. Ein moderner Monolith mit React-Views, die aus Laravel-Controllern gerendert werden.
Wenn Du Backend und Frontend in zwei per API verbundene Repositories trennst, gewinnst Du eine Sache und verlierst fünf. Du gewinnst Freiheit, morgen ein anderes Frontend aufzusetzen (eine mobile App, eine andere Marke mit derselben API). Du verlierst: einheitliche Authentifizierung, geteilte Validierung, triviales SEO, atomares Deployment, serverseitigen Session-Zustand und die Fähigkeit, ein Feature in einem einzigen Commit zu iterieren. Für eine authentifizierte interne Anwendung wie Nexo wiegen diese fünf Dinge viel mehr als die theoretische Flexibilität.
Inertia löst die Passform. Die Laravel-Controller geben statt Blade oder JSON Inertia::render('PageName', $props) zurück. Das rendert eine konkrete React-Komponente (resources/js/Pages/Workspace/Dashboard.tsx) mit den Props, die das Backend bereits berechnet hat. Navigieren ist ein interner Fetch, der Komponente und Props ohne Neuladen austauscht: SPA-UX, Wahrheit auf dem Server, Authentifizierung per Cookies und Laravel-Middleware wie eh und je.
Die HandleInertiaRequests-Middleware teilt globale Props (Nutzer, Workspace, Rolle, Flags) mit allen Seiten, sodass keine Komponente diesen Zustand auf jeder Seite anfordert, noch wir ein Redux/Zustand für etwas aufsetzen, was der Server bereits weiß. Einheitliches Deployment: git pull, composer install, npm run build, Cache-Bereinigung, ohne Inkohärenzfenster zwischen API und Client.
Die Kosten sind, dass wir, wenn wir Nexo für externe Integratoren öffnen, parallel eine formale API brauchen. Wir haben sie früh aufgesetzt: REST API `/api/v1` mit Laravel Sanctum (Tasks, Projekte, Datensätze, Docs, Leads, Billing, Webhooks), die neben den Inertia-Routen lebt. Eigenes Frontend per Inertia, Integratoren per REST mit Tokens.

Das Multi-Tenant-Modell: Organization → Workspace → Member
Die zweite strukturelle Entscheidung ist, wie Du Multi-Tenancy machst. Zwei konkurrierende Schulen: Shared Database mit Tenant-Spalte gegen Schema per Tenant. Wir haben die erste gewählt, mit einer dreistufigen Hierarchie.
- Organization: optionale Gruppierung für Enterprise-Konten mit mehreren Workspaces unter derselben Marke.
- Workspace: die echte Einheit der Tenancy. Eigene Daten, Konfiguration, Module und visuelle Identität. Identifiziert durch
slugin der URL:/w/kiwop/dashboard. - WorkspaceMember: Pivot-Tabelle, die Nutzer und Workspaces mit einer Rolle verbindet (owner, admin, comercial, operations, member, viewer).
Wir haben Shared Database mit Spalte workspace_id aus drei pragmatischen Gründen gewählt: das Modell ohne Migration von N Schemata weiterentwickeln, produktübergreifende Analysen ohne föderierte Queries und eine einzige Backup/Restore-Operation. Die Kosten sind, dass ein Bug, der einen Filter nach workspace_id auslässt, theoretisch Daten zwischen Tenants kreuzen kann. Wir mitigieren das mit zwei Teilen.
Der erste ist die Middleware `EnsureWorkspaceContext`, die den Slug der URL in das Workspace-Modell auflöst, die Mitgliedschaft des Nutzers verifiziert und, wenn der Workspace zu einer Organisation gehört, auch die Mitgliedschaft darin. Sie scheitert mit 403, wenn etwas nicht passt. Läuft auf allen Routen unter /w/{workspaceSlug}/*.
Der zweite ist das Trait `WorkspaceScoped`, das auf sensible Eloquent-Modelle (BrainProtocol, BrainProtocolRun etc.) angewendet wird, sodass jede Query standardmäßig where workspace_id = ? anwendet. Das Deaktivieren erfordert, es explizit zu tun, und das sieht man im Code Review.
Es gibt ein operatives Detail, das wir als absolute Regel dokumentieren: Die Workspace-Controller müssen string $workspaceSlug als zweiten Parameter enthalten (nach Request $request) vor jedem anderen. Laravel injiziert Parameter positionell, und das Auslassen des Slugs führt dazu, dass der nächste Parameter ($id) den Slug erhält und die Anfrage mit einem sehr schwer zu diagnostizierenden stillen 404 stirbt. Die Index-Route funktioniert, was den Bug nur beim Öffnen des ersten Details erscheinen lässt.

Die folgende Tabelle fasst die grundlegenden Architekturentscheidungen zusammen, die wir in Nexo getroffen haben, mit dem Grund und dem akzeptierten Trade-off. Wenn Du ein ähnliches System entwirfst, ist diese Tabelle wahrscheinlich der Teil des Artikels, den Du am besten in ein internes Dokument kopierst-einfügst.
Module Toggle System: Features pro Workspace aktivieren/deaktivieren
Jeder Workspace aktiviert oder deaktiviert Module unabhängig. Der Mechanismus: eine JSON-Spalte enabled_modules in der Tabelle workspaces, die Modul auf Boolean mappt. Die Liste ist geschlossen und lebt im Code als Konstante Workspace::MODULES (wir lassen den Nutzer nicht neue Module zur Laufzeit erstellen). Sie umfasst die Core-Blöcke (dashboard immer aktiv, fichaje, registros, solicitudes), Analytik (rentabilidad, informes), Arbeit (projects, tasks), Vertrieb (leads, outbound, licitaciones), Finanzen (billing_contracts, billing, contabilidad), Werkzeuge (booking, decision_requests, chat), Wissen (docs), Settings (branding, notifications, settings), HR (employees, practicas, personal_documents, payroll) und neuere Module wie chatbot, pulse_surveys, proactive_agents, capacity_planner, project_pulses und meetings.
Zwei Stellen fragen den Toggle ab: die Methode isModuleEnabled('leads') des Modells und vor allem die Middleware `EnsureModuleEnabled`, die auf die Routengruppen angewendet wird:
Die Middleware überprüft zwei Dinge in Reihenfolge: (1) ob das Modul für den Workspace aktiviert ist und (2) ob die Rolle des Nutzers in diesem Workspace auf das Modul zugreifen darf. Bei jedem Fehlschlag gibt sie 404 zurück (nicht 403, um die Existenz der Route nicht zu verraten). Es ist eine kleine Maßnahme der Security through Obscurity, die, kombiniert mit den anderen Schichten, die Hürde der Aufklärung für einen internen Angreifer erhöht.
Warum wir keine klassischen Feature Flags (LaunchDarkly, Unleash, Flagsmith) verwenden: Unsere Toggles sind keine A/B-Experimente oder graduellen Roll-outs, sondern Produktkonfiguration pro Tenant. Ein Kunde ohne Payroll-Modul wird es nicht haben, bis er es abschließt. Einen externen SaaS einzufügen, um das zu verwalten, wäre kostenlose Komplexität.
Das branding-Modul verdient eine eigene Erwähnung: Wenn es aktiv ist, kann der Workspace seine Primärfarbe anpassen (CSS-Variable --workspace-primary), ein Logo hochladen und die visuelle Identität anpassen. Nützlich für White-Label oder Enterprise-Kunden, die die interne Marke wollen. Das Basis-CSS bleibt erhalten, nur der Akzent ändert sich.
RBAC: Rollen + Berechtigungen ohne externe Bibliotheken
Für die Autorisierung verwendet Nexo weder Spatie Laravel-permission noch eine andere Bibliothek. Wir haben ein eigenes RBAC-System: Tabellen roles, permissions und role_permissions, mit festen Rollen (owner, admin, comercial, operations, member, viewer) und granularen Berechtigungen, die in RbacSeeder definiert sind. Drei Gründe, keine Bibliothek zu verwenden.
Erstens ist die Berechtigungsdomäne an eigene Konzepte gekoppelt: Workspace, Organisation, Module, Aktionen auf Entitäten. Es ist nicht "Nutzer kann X tun", sondern "dieser Nutzer, in diesem Workspace, mit dieser Rolle, kann X an Y tun". Mit einer generischen Bibliothek hätten wir am Ende die Hälfte der Schichten darüber umgeschrieben.
Zweitens löst das Gate von Laravel bereits die Autorisierung. Wir definieren Gates im AuthServiceProvider, die auf Policies zeigen (DocumentationPolicy::viewCategory), und verwenden Gate::allows(...) oder @can(...) in den Views. Das Einzige, was wir hinzufügen, ist die Auflösung der Workspace-Rolle: $workspace->getUserRole($user) und $workspace->canRoleAccessModule($role, 'leads'), gecacht in $request->attributes während des Request-Lebenszyklus.
Drittens haben wir einen Simulationsmechanismus ("die App wie ein anderer Nutzer sehen", nützlich im Support), der mit dem RBAC auf spezifische Weise interagiert: Wenn der Admin simuliert, wird der übliche "Admin kann alles"-Bypass absichtlich deaktiviert, und Du siehst die App so, wie der simulierte Nutzer sie sieht. Das lebt in EnsureModuleEnabled mit $request->attributes->get('is_simulating', false). Über einer externen Bibliothek wäre es fragil gewesen.
Legacy Coexistence: altes Blade + neues Inertia parallel
Nexo hat eine Vorgeschichte. Die Originalversion der Anwendung wurde in Laravel 10 mit Blade + jQuery + Bootstrap gebaut. Das Inertia/React-Frontend ist später. Die Migration wurde nicht auf einen Schlag gemacht: Es gibt immer noch lebendigen Blade-Code in Produktion. Diese Koexistenz ist die Realität jeder echten Anwendung, die mehrere Jahre in Produktion ist.
Die Frage ist, wie Du verhinderst, dass diese Koexistenz zu einem Problem wird. Unsere Antwort ist die Middleware `LegacyRouteGuard`, registriert als legacy.guard, angewendet auf alle alten Routen. Sie tut drei Dinge:
- Loggt jeden Zugriff auf Legacy-Routen in
storage/logs/legacy-routes-*.log. Echte Telemetrie darüber, welche alten Routen noch leben, leitet die Priorisierung. - Blockiert POST/PUT/PATCH/DELETE mit 409 Conflict und Nachricht "Legacy interface is read-only". Keine Datenänderung geht über den alten Weg.
- Leitet GETs auf das neue Äquivalent um, wenn ein Mapping definiert ist. Die
redirectMapassoziiert Legacy-Route mit Workspace-Route ('admin.horarios' => 'workspace.registros.index', etc.) und löst Parameter (workspaceSlug,articleId,contract) zur Laufzeit auf.
Die Schlüsselregel ist "GETs offen, Writes geschlossen". Wenn Du alles blockierst, brichst Du Bookmarks und alte Links in E-Mails. Wenn Du Writes erlaubst, hast Du zwei Wege, dieselben Daten zu ändern, und kannst nicht garantieren, dass die Validierungen, Events und Audit-Logs des neuen Wegs ausgelöst werden. Die Kombination "lies über den alten Weg, schreibe nur über den neuen" ermöglicht es, Bookmarks nicht zu brechen, während wir Views eine nach der anderen ohne Angst migrieren.
Es gibt eine Ausnahme: Die Admin-Simulationsrouten (/admin/simulate-employee, /admin/stop-simulation, /admin/toggle-view-mode) tragen den Guardian nicht, weil sie legitime Support-Türen sind. Der Rest ja.
Es ist ein replizierbares Muster: Wenn Du eine Laravel-App von 2019 mit Blade hast und zu Inertia migrieren möchtest, ohne das Business sechs Monate anzuhalten, füge Guardian-Middleware hinzu, blockiere Writes, logge Zugriffe, mappe Redirects und greife die Migration nach echter Zugriffsfrequenz an (nicht nach alphabetischer Reihenfolge oder ästhetischem Geschmack).

Brain — das KI-Agenten-System von Nexo
Hier kommt das eigenste Stück von Nexo ins Spiel: Brain, ein Multi-Step-KI-Agenten-Motor, der innerhalb der Anwendung lebt. Es ist kein LangChain-Wrapper noch ein generisches Framework. Es ist ein Motor, der entwickelt wurde, um Automatisierungen über den Daten eines Workspace auszuführen, mit expliziten Limits und vollständiger Nachverfolgbarkeit.
Die Domäne besteht aus zwei Modellen: `BrainProtocol` (Definition: Name, Beschreibung, Steps, Konfiguration) und `BrainProtocolRun` (Ausführung: Kontext, Ergebnisse Schritt für Schritt, Status, Tokens). Beide leben unter workspace_id mit dem Trait WorkspaceScoped, sodass sie sich niemals zwischen Tenants kreuzen.
Der Motor `BrainProtocolEngine` verarbeitet Steps sequenziell mit einem mutablen Kontext im Speicher. Jeder Step liest aus dem Kontext, führt seine Logik aus und schreibt unter einem output_key, den die folgenden Steps lesen können. Er wendet zwei in Stein gemeißelte harte Limits an:
Ein Protokoll mit mehr als zehn Steps wird bei der Ausführung gekürzt. Wenn der akkumulierte Kontext 300.000 Zeichen überschreitet, loggen wir Warning und kürzen ihn vor dem Modellaufruf mit einem Marker [CONTEXTO TRUNCADO POR LÍMITE DE TAMAÑO]. Diese beiden Limits sind die Barriere gegen das realste Risiko eines Agenten: die kombinatorische Explosion von Kontext und Kosten, wenn er über seine eigenen Outputs iteriert.
Brain unterstützt sechs Step-Typen:

Der Step ai_call ist der, der die meiste Arbeit leistet. Er liest die als input_keys markierten Schlüssel, serialisiert sie zu JSON unter Überschriften ### schlüssel und fügt sie einem Abschnitt ## DATOS DISPONIBLES hinzu, der an den Prompt angehängt wird. Das macht den Prompt-Kontext-Vertrag explizit, verhindert, dass das Modell Daten außerhalb des Abschnitts erfindet, und erleichtert das Debuggen: der exakte Kontext bleibt im Audit Trail.
Der Step action ist das, was Brain zu "einem Agenten" macht und nicht zu einer Pipeline. Unterstützte Typen: create_task, create_proposal, save_memory, alert. Jeder löst Parameter mit Templates {{key.subkey}} gegen den Kontext auf: Der Titel einer Task kann {{ai_summary.content}} sein, und der Motor ersetzt ihn vor der Ausführung. Verbindet das Reasoning des Modells mit der echten Aktion auf der Domäne.
Der Step condition ist die Brandmauer. Mit skip_remaining_if_false: true, wenn die Bedingung nicht erfüllt ist, wird der Rest des Protokolls übersprungen und die Steps werden als skipped_remaining markiert. Erlaubt Protokolle vom Typ "wenn nichts zu tun ist, verschwende keine Tokens".
Beispiel: Lead-Onboarding-Protokoll. (1) connector_fetch neuer Leads der letzten 24 Stunden. (2) condition, die abbricht, wenn die Liste leer ist. (3) data_filter, der Leads mit Score > 60 behält, Limit 20. (4) ai_call, der einen ersten E-Mail-Entwurf generiert. (5) action vom Typ create_proposal, die den Vorschlag in den Posteingang des Vertriebsmitarbeiters legt — keine gesendete E-Mail, weil Aktionen mit externem Effekt durch menschliche Genehmigung laufen.
Alles wird in BrainProtocolRun registriert: finaler Kontext, Ergebnisse jedes Steps mit Dauer und Status, Tokens, Nutzer, Trigger-Quelle (manual, schedule, connector_update, agent_alert) und Zeitstempel. Dieser Audit Trail ist die Tabelle, über die ein Compliance-Officer auditieren kann, was die KI über die Kundendaten getan hat. Ohne ihn ist ein SaaS mit Agenten nicht auditierbar; mit ihm ist er es bis ins feinste Korn. Brain ist unsere interne Materialisierung vieler der Muster, die wir in KI-Agenten in Produktion: Muster und Antimuster für 2026 dokumentieren.
KI-Vertragsextraktion + Anti-Halluzination
Das andere wichtige KI-Stück in Nexo ist älter als Brain und löst einen konkreten Fall: Anlegen eines Abrechnungsvertrags aus seinem PDF. Die manuelle Arbeit, Kundenname, Betrag, Daten, e-Fact-Daten, Bürgschaften und Meilensteine zu kopieren, ist klebrig und fehleranfällig.
Das Subsystem `AiContractExtraction` löst diesen Flow. Ein Endpoint POST /billing/contracts/ai-extract empfängt das PDF (maximal 10MB), lässt es durch PdfTextExtractor laufen (basierend auf smalot/pdfparser) und übergibt es an den von AiExtractorFactory gewählten Extraktor. Die Factory wählt zwischen OpenAiContractExtractor (GPT-4o in seiner aktuellsten stabilen Version, konfigurierbar per OPENAI_MODEL) oder seinem Anthropic-Äquivalent je nach AI_PROVIDER. Das Interface ContractExtractorInterface garantiert, dass der Anbieterwechsel ein Flag ist.
Der Prompt ist für Verträge in Spanisch optimiert, mit expliziten Anti-Halluzinations-Regeln. Die Antwort ist strukturiertes JSON, in dem jedes Feld ein Objekt mit value, confidence (0-1) und evidence (wörtliches Zitat aus dem PDF-Text) ist. Das ändert die UX radikal: Das Formular wird nur automatisch ausgefüllt, wenn confidence >= 0.5, die ausgefüllten Felder erscheinen mit hellblauem Rahmen und Tooltip "Von KI ausgefüllt (Konfidenz: 85%)", und ein Akkordeon "was hat die KI gefunden?" zeigt die wörtliche Evidenz. Der Mensch vertraut niemals blind: überprüft, passt an und speichert.
Die Anti-Halluzinations-Regeln des System-Prompts sind der Kern dafür, warum das funktioniert. Wenn die KI ein Feld nicht findet, muss der Wert null sein und confidence muss 0 sein. Wenn sie einen Wert aus dem Kontext ableitet (21% MwSt. annehmen, wenn nicht aufgeschlüsselt), muss sie die Confidence senken und einen Eintrag zu warnings hinzufügen. Die Warnings werden vor dem Speichern angezeigt: "Keine aufgeschlüsselte MwSt. gefunden, 21% angenommen", "Enddatum nicht klar angegeben". Die Kombination Confidence + Evidence + Warnings + obligatorische menschliche Überprüfung ist das, was eine LLM-Extraktion in einer Umgebung betreibbar macht, in der ein Fehler Dich zur fehlerhaften Rechnungsstellung führt.
Alles bleibt in der Tabelle ai_contract_extractions: wer hochgeladen hat, wann, welche Datei (mit SHA256-Hash; wir speichern den vollständigen Text nicht, wichtig für Datenschutz), welcher Anbieter, welches Modell, Tokens, geschätzte Kosten, JSON-Ergebnis, Warnings, Status. Wenn jemand fünf Monate später fragt "Wie sind diese Daten in den Vertrag gekommen?", liegt die Antwort in der Datenbank.
Der allgemeine Motor dahinter — der, der den Swap zwischen OpenAI und Anthropic mit einem Flag erlaubt, der Budgetlimits verwaltet (AiBudgetGuard), der auflöst, welche Verbindung je nach Workspace verwendet wird (AiConnectionResolver) — ist das `AiGateway`, dasselbe, das die ai_call von Brain verwenden. Die LLM-Ausgabe zu zentralisieren gibt drei Dinge: einen einzigen Punkt für Rate Limits und Budgets, einen einzigen Punkt zum Loggen von Tokens pro Feature (chat, agent, extraction) und einen einzigen Punkt zum Wechseln des Anbieters, ohne Business-Code zu berühren. Wenn wir morgen ein Sonnet 5 oder ein GPT-6 testen, lebt die Änderung in einer einzigen Zeile.
Diese Abstraktion wenden wir auch in Kundenprojekten unter LLM-Integration und Entwicklung von KI-Agenten an. Die Ausgabetür zum Modell zu zentralisieren ist einer der ersten Ratschläge, die wir jedem geben, der KI in ein Produkt in Produktion integriert.
Integration mit Holded ERP (bidirektional)
Nexo ersetzt nicht das Fakturierungs-ERP. Es ersetzt das Verwaltungspanel, das auf das ERP schaut. Für echte Fakturierung nutzen wir weiterhin Holded und synchronisieren bidirektional damit. Die Integration lebt unter app/Services/Holded/ mit vier Teilen: HoldedClient (HTTP-Client), HoldedSyncService (Orchestrator), HoldedStatusMapper (Status-Mapping) und SyncResult / BatchSyncResult (typisierte Ergebnisse).
Die Synchronisation folgt dem klassischen Muster der Integration mit externem ERP: Jobs in Queue mit Retries, geplante Synchronisation alle 15 Minuten und ein Artisan-Command holded:sync-invoices --sync, um eine manuelle Runde zu erzwingen. Die bidirektionale Richtung bedeutet zwei Flows: Von Nexo zu Holded, wenn ein Meilenstein als "ausgestellt" oder manuell "bezahlt" markiert wird (die Rechnung wird erstellt), und von Holded zu Nexo, wenn ein Kunde eine Rechnung im ERP bezahlt und die Statusänderung in der nächsten Runde zurückkommt.
Die Konfliktverwaltung ist, wo diese Integrationen schmutzig werden. Unsere Regel: "Holded gewinnt bei Finanzstatus, Nexo gewinnt bei kommerzieller Metadata". Den Zahlungsstatus entscheidet Holded (offizielles Finanzsystem). Die internen Labels, Notizen und Verknüpfungen zu Projekten entscheidet Nexo. Diese Teilung reduziert Konflikte auf eine überschaubare Teilmenge, in der eine der beiden Seiten immer autoritativ ist.
HOLDED_ENABLED aktiviert oder deaktiviert die Integration pro Umgebung. Die Konfiguration erfolgt auf Workspace-Ebene, nicht global — jeder Workspace kann auf seine eigene Holded-Instanz mit seinem eigenen Key zeigen.

Observability und Betrieb in Produktion
Wenn Du es nicht betreiben kannst, hast Du es nicht. Nexo läuft in Produktion unter nexo.kiwop.com mit einem einfachen Stack: PHP 8.4 FPM mit dediziertem Pool, MySQL/MariaDB 10.4, Node 20 via nvm für Frontend-Build und nginx davor. In der Entwicklung DDEV mit Replikation des Stacks in Docker. Begrenzte Dev/Prod-Parität (PHP 8.2 lokal vs 8.4 Produktion).
Die tägliche Observability stützt sich auf drei Quellen. storage/logs/laravel.log ist das Haupt-Log (Fehler, Exceptions, Warnings des Brain-Engines, wenn der Kontext die Größe überschreitet). storage/logs/legacy-routes-*.log ist das dedizierte Log für Zugriffe auf Legacy-Routen, täglich rotiert: Wenn eine Route in zwei Wochen nicht erscheint, ist sie Kandidat zum Löschen. Und BrainProtocolRun ist der strukturierte Audit Trail jeder Agent-Ausführung, per SQL konsultierbar.
Das Deployment folgt ./scripts/deploy.sh: artisan down, DB-Backup, git pull, composer install --no-dev, Frontend-Build, wenn Deps sich geändert haben, artisan migrate --force, Caches, artisan up. Rollback: letztes mysqldump wiederherstellen und git checkout <sha>. Niedrige Deploy-Rate, menschliche Überprüfung pro Deploy.
Ein interessantes operatives Stück ist das Claude-CLI-Relay (claude-relay.service). Einige Workspaces verwenden KI-Verbindung per CLI-Authentifizierung (OpenClaw-Stil) statt API-Key, um ein dediziertes Claude Max-Abonnement zu konsumieren. Das Relay ist ein PHP-Daemon unter systemd als Nutzer proves (niemals root), der interne HTTP-Requests in Aufrufe an das CLI claude -p übersetzt. Muster zur Entkopplung der KI-Kosten von der tatsächlichen Plattformnutzung.
Skalierung und gewonnene Lektionen
Es lohnt sich innezuhalten und zu erzählen, was wehgetan hat, was wir anders machen würden und was wir hinzufügen würden, wenn wir heute anfingen.
Was heute wehtut. Die Workspace-Controller erfordern, die Signatur (Request $request, string $workspaceSlug, ...) zu merken. Menschlicher Fehler, der oft genug gemacht wurde. Mit Perspektive hätten wir es mit Dependency Injection des CurrentWorkspace DTO statt des Lesens des Slug aus dem Routenparameter gelöst — EnsureWorkspaceContext registriert es bereits, wir verwenden es nur nicht konsistent als einzige Tür. Die Controller auf dieses Muster zu migrieren ist ausstehende Arbeit.
Die Blade + Inertia-Koexistenz hat länger gedauert als geplant. Der ursprüngliche Plan war, die gesamte App in sechs Monaten zu migrieren. Heute, mit der Guardian-Middleware, die Writes seit über einem Jahr blockiert, existieren immer noch Blade-Read-Only-Views, die niemand priorisiert hat zu migrieren. Es ist kein Sicherheitsproblem, aber ja ein Wartungsproblem (zwei parallele Stacks) und UX-Konsistenz. Lektion: Die inkrementelle Migration mit Guardian funktioniert, aber benötigt expliziten Kalender und einen Owner; ohne klaren Verantwortlichen hängt die Migration fest.
Was wir nicht hinzufügen würden. Wir haben widerstanden, ein externes Agenten-Framework (LangChain, CrewAI, Haystack) für Brain einzufügen. Brain tut genau, was wir brauchen, und ein generisches Framework hätte die Oberfläche multipliziert, ohne funktionale Fähigkeit hinzuzufügen. Für einen Agenten, der innerhalb einer Laravel-App mit eigener Domäne lebt, wiegt ein maßgeschneiderter Motor aus wenigen gut verstandenen Zeilen weniger als ein Framework aus Tausenden von Zeilen, das Du nicht kontrollierst.
Was wir hinzufügen würden. Wir beginnen, über WebMCP als externes Protokoll nachzudenken, um Brain Drittanbieter-Agenten (Claude Code, Cursor, einem Kunden-PA) bereitzustellen. Wenn Brain bereits autonome Arbeit auf dem Workspace mit vollständigem Audit Trail ausführt, würde es die Bereitstellung als MCP Server einem externen Agenten ermöglichen, Nexo-Protokolle als native Tools auszuführen. Es ist das Muster, das wir in dem Artikel über MCP, WebMCP und A2A analysieren. Arbeit der kommenden Monate.
Was wir nicht veröffentlicht haben. Wir geben keine konkreten Zahlen zu aktiven Workspaces, Nutzern, monatlichen Tokens oder Protokoll-Runs pro Tag. Nicht aus Geheimhaltung: Jede Zahl wird in drei Monaten veraltet sein, und wir geben lieber keine Metriken, die zu "falschen historischen Daten" werden. Wenn es für eine technische Bewertung vor einem Projekt relevant ist, frag uns direkt.
Skala. Nexo ist kein massiver SaaS: Dutzende B2B-Mid-Market-Workspaces, jeder mit Dutzenden bis niedrigen Hunderten von Nutzern. Die Architektur unterstützt komfortabel eine Größenordnung mehr ohne Redesign; der echte Engpass — in der Größenordnung von Tausenden von Workspaces — wäre die Synchronisation mit Holded (wegen Rate Limit der externen API, nicht wegen Nexo) und die Cache-Schicht.
Häufig gestellte Fragen
Warum Inertia.js statt einer separaten SPA mit REST-API?
Weil Nexo eine authentifizierte interne Anwendung ist, kein massives öffentliches Produkt. Inertia gibt SPA-UX mit atomarem Deployment, einheitlicher Auth, geteilter Validierung und serverseitiger Session. Eine separate SPA macht nur Sinn, wenn Du mehrere Frontends über derselben API benötigst oder wenn Frontend und Backend in unterschiedlicher Infrastruktur leben — keines davon trifft zu. Parallel dazu halten wir eine REST-API /api/v1 für externe Integratoren.
Ist Shared-Database mit Spalte `workspace_id` sicher gegenüber Schema pro Tenant?
Es ist sicher, wenn Du Disziplin anwendest: Middleware, die den Workspace in jeder Request auflöst, Trait WorkspaceScoped, das den Filter standardmäßig anwendet, und Code Review, das das Deaktivieren des Scope blockiert. Die Kosten von Schema pro Tenant (Migrationen N-mal, Backups pro Tenant, föderierte Analysen) haben sich nicht gelohnt. Für sehr hohe Lasten oder spezifische regulatorische Anforderungen (Healthcare) ändert sich die Rechnung.
Was tut ein `BrainProtocol` und wie wird es konfiguriert?
Ein Protokoll ist eine Sequenz von bis zu 10 typisierten Steps (connector_fetch, data_filter, ai_call, db_query, action, condition), die der Motor sequenziell unter Teilen eines mutablen Kontexts ausführt. Es wird vom Workspace-Panel aus konfiguriert, indem der Trigger (Konnektor, Schedule oder manuell) gewählt, die Steps in Reihenfolge definiert und markiert wird, welche kritisch sind. Jeder Run generiert einen BrainProtocolRun mit vollständigem Audit Trail.
Wie viel kostet Brain in Tokens pro Monat?
Hängt von der Nutzung und vom Modell ab. Die Limits MAX_STEPS = 10 und MAX_CONTEXT_CHARS = 300000 sind die harten Brandmauern, und jeder ai_call enthält sein eigenes max_tokens (standardmäßig 2000). Für das aktuelle Nutzungsprofil sind die Kosten eine kleinere Ausgabenposition und werden pro Feature (chat, agent, extraction) über das AiGateway überwacht.
Wie verwaltet Ihr die Integration mit Holded, wenn Holded ausfällt?
Die Synchronisations-Jobs sind in Queue mit Retries. Wenn Holded ausgefallen ist, degradiert die Synchronisation, ohne Nexo zu brechen: Die Meilensteine können weiterhin erstellt und geändert werden, die Erstellung in Holded bleibt in der Queue und wird wiederholt, wenn die API antwortet. Der sichtbare Status ist "pending sync". Wir blockieren den Workspace-Betrieb nicht wegen eines ERP-Ausfalls.
Warum habt Ihr das alte Blade über ein Jahr lang beibehalten?
Weil ein Big-Bang-Rewrite einer App in Produktion mit echten Daten ein Antimuster mit Horrorgeschichten ist. Die Middleware legacy.guard blockiert Writes seit Tag eins und eliminiert das Risiko von Inkonsistenzen. Read-Only-Views zu migrieren ist Arbeit ohne kritisches Datum, geleitet von echten Nutzungs-Logs. Die Alternative — Features sechs Monate stoppen — wäre kommerziell viel schlechter gewesen.
Was passiert, wenn ein Entwickler `string $workspaceSlug` in einem Workspace-Controller vergisst?
Die Anfrage kommt beim Controller an, aber Laravel injiziert den Slug in den nächsten Parameter ($id), der Lookup schlägt fehl und die Anfrage gibt 404 zurück. Symptom: stiller 404, sehr schwer zu diagnostizieren. Die Index-Route funktioniert, also erscheint der Bug nur beim Öffnen des ersten Details. Es ist als absolute Regel dokumentiert und das Erste, was wir jedem neuen Entwickler erklären.
Kann man Brain als MCP Server bereitstellen, damit ein externer Agent ihn nutzt?
Heute nicht, es ist auf der Roadmap. Das technische Stück existiert (Motor, Audit Trail, per-workspace Limits) und der natürliche Schritt ist, es in einen MCP Server zu wickeln, der RBAC und Modul-Toggles respektiert. Öffnet Nexo für externe Agenten-Ökosysteme, ohne die Datenbank zu öffnen. Wenn Dich der Use Case für Deinen eigenen SaaS interessiert, ist es Teil der Entwicklung von KI-Agenten und der KI-Beratung.
Fazit: Einen SaaS 2026 zu bauen bedeutet nicht, Mode zu wählen, sondern Disziplin
Die ehrliche Schlussfolgerung, Nexo von einer Laravel 10-App mit Blade in eine vollständige operative Plattform mit Laravel 12, Inertia + React und KI-Agenten zu verwandeln, ist, dass die technische Architektur viel weniger wiegt, als die Leute bei der Stack-Wahl glauben, und viel mehr, als die Leute bei seiner Wartung glauben. Nichts von dem, was wir hier erzählen, ist exotisch: Laravel ist seit einem Jahrzehnt reif, Inertia ist ein einfaches Stück, Multi-Tenancy mit Spalte workspace_id ist das Standardmuster des PHP-Ökosystems, RBAC mit Tabellen roles + permissions + role_permissions ist in jedem Kurs zum Datenbankdesign zu finden. Der Unterschied zwischen einem SaaS, der skaliert, und einem, der steckenbleibt, liegt nicht im Namen des Frameworks — er liegt in der Disziplin, mit der Du die Middleware anwendest, die Querzugriffe blockiert, in den expliziten Limits, die Du in Stein meißelst, wenn Du KI in den Flow einfügst, und in Deiner Fähigkeit, mit Legacy-Code zu koexistieren, ohne ihn in eine Zeitbombe zu verwandeln.
Nexo ist das Produkt, das am besten illustriert, wie wir arbeiten. Wenn Dein Unternehmen überlegt, einen multi-tenant SaaS zu bauen, wenn es KI-Agenten auf auditierbare Weise in eine Anwendung einführen muss, die bereits Jahre in Produktion ist, oder wenn Du Nexo direkt als operative Plattform evaluierst, lass uns reden. Wir sind Kiwop, Digitalagentur spezialisiert auf Softwareentwicklung und angewandte Künstliche Intelligenz für globale Kunden in Europa und den USA, und wir bieten maßgeschneiderte Entwicklung von KI-Agenten, Beratung für Künstliche Intelligenz, LLM-Integration und Enterprise RAG für Produktionskontexte an. Du kannst die öffentliche Erfolgsgeschichte von Nexo sehen oder uns direkt schreiben.