Back to docs
Guide 02

Architecture

How Simmetric Chat is put together.

Simmetric Chat è un modular monorepo TypeScript con 5 package + un'app desktop Tauri. Il principio guida è la separazione dei confini: ogni package ha responsabilità distinte e comunica con gli altri solo tramite interfacce ben definite (HTTP o il package shared).

Vista d'insieme

┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│   frontend   │   │    widget    │   │  src-tauri   │
│  React 19    │   │  Preact IIFE │   │  Desktop app │
│  Vite :5173  │   │  Express:3211│   │              │
└──────┬───────┘   └──────┬───────┘   └──────┬───────┘
       │  /api (JWT)     │  /api/internal/widget (X-Api-Key) │
       │                 │                                  │
       ▼                 ▼                                  │
┌────────────────────────────────────┐                      │
│              server                │◄──── Tauri usa il ────┘
│  Express :3000  (RBAC, agent, RAG) │     server via HTTP
└──────┬──────────────────┬──────────┘
       │ HTTP (COLLECTOR_SECRET)     │ HTTP (status callback)
       ▼                             ▼
┌──────────────┐             ┌──────────────┐
│   collector  │             │  PostgreSQL  │
│  Express:3210│             │  (Prisma)    │
│  parse/embed │             └──────────────┘
└──────────────┘
       │
       ▼
┌──────────────────────────┐
│  Vector DB (LanceDB/     │
│  Qdrant) + LLM (Ollama/  │
│  OpenAI/Anthropic)       │
└──────────────────────────┘

I 5 package

shared — il leaf node

Tipo, Zod schema e costanti. Zero business logic, unica dipendenza runtime zod. È importato da server, collector, frontend. Mai importare da altri package: la direzione è sempre * → shared. Questo garantisce build order Turborepo e previene cicli.

  • src/types/ — interfacce entità (User, Workspace, Chat, Document, Provider, Widget, BackupDestination, Archive, OcrJob, SynthesisRun, ...).
  • src/schemas/ — Zod (auth, chat, document, widget, mcpConnection, license, backup, archive, wiki, ocr, synthesis, provider). Una schema per dominio, type inference z.infer.
  • src/constants/permissions.ts (27 permessi RBAC), license.ts (15 feature flag).

server — il cuore (Express :3000)

API REST + RBAC + orchestratore agent. Vedi packages/server/CLAUDE.md.

  • src/routes/ — un file per dominio (auth, users, roles, projects, workspaces, documents, agent, chat, providers, apiKeys, eventLogs, analytics, backups, backupJobs, backupDestinations, webhooks, push, license, sso, templates, settings, mcpConnections, marketplace, widgets, internalWidget, health, ocr, archives, wiki, synthesis).
  • src/middleware/auth.ts (JWT + API key), rbac.ts (permessi + IDOR prevention), rateLimit.ts (200/min generale, 10/min auth, chat Community 20 / Enterprise 100 /min), license.ts (feature flag gating), widgetCors.ts (CORS dinamico per-origin).
  • src/services/ — authService, providerService (risoluzione modello + capability), hybridSearchService (RRF), ftsService (tsvector), encryptionService (AES-256-GCM), backupSchedulerService (Bree), eventLogService (+ webhook dispatch), dlpFilter, ecc.
  • src/agent/orchestrator.ts (loop ReAct), builtinSkills.ts (rag_search, workspace_memory, document_temp_process), llmStreaming.ts (Ollama/OpenAI/Anthropic), mcpClient.ts (connessioni MCP outbound), mcpServer.ts (RAG come tool MCP per IDE).
  • src/config/env.ts — schema Zod per tutte le env var; process.exit(1) se invalide.

collector — microservice ingestion (Express :3210)

Parla con il server solo via HTTP (segreto COLLECTOR_SECRET). Non importa Prisma né servizi del server: boundary critico per il deploy air-gap su nodi separati.

  • src/routes/ingest.ts — upload (multer), query, youtube, delete.
  • src/services/ — parser (PDF/DOCX/PPTX/XLSX/TXT/CSV/YouTube), chunker (RecursiveCharacterTextSplitter), embeddings (Xenova locale / OpenAI), vectorStore (LanceDB / Qdrant).
  • Flusso: server → collector POST /api/ingest/upload → parse → chunk → embed → store → collector callback PUT /api/documents/:id/status → server aggiorna DB.

widget — chat embeddabile (Express :3211)

Express CommonJS + Preact IIFE bundle. Autenticato al server via X-Api-Key.

  • src/index.tscreateApp() factory; static /widget/app.js + loader routes + API.
  • src/routes/ — chat (SSE proxy), session, config, loader, lead.
  • src/widget/ — componenti Preact (12 componenti) in iframe sandboxed.
  • Proxy SSE trasparente: relay byte grezzi server→iframe, zero parsing.

frontend — SPA (Vite + React 19 :5173)

Niente Next.js. Proxy /api → server.

  • State management 3-tier: TanStack Query (REST/CRUD), React Context (UI state), fetchEventSource + useState/useRef (SSE, NON TanStack Query).
  • src/queries/ (22 hook), src/contexts/, src/hooks/useChat.ts (streaming).
  • i18n: 7 lingue (en, it, ru baseline + de, fr, es, zh).

src-tauri — desktop app

Wrapper Tauri che impacchetta il frontend come app desktop; comunica con il server via HTTP (stessa API). Pensato per deploy air-gap su macchine senza browser.

Data flow principali

Chat streaming (utente interno)

Frontend useChat → POST /api/workspaces/:id/chat/stream (SSE)
  → requireWorkspaceAccess (RBAC/IDOR)
  → agent orchestrator (ReAct loop)
     ├─ resolveSkillsForChat (builtin + MCP pinned)
     ├─ rag_search (vector + tsvector → RRF merge) se serve
     ├─ llmStreaming (provider risolto: per-chat → workspace → global → ENV)
     └─ tool execution (sandboxed)
  → SSE: token | status | citations | done(modelUsed,providerUsed,mcpSources) | error

Risoluzione modello: per-chat override → workspace default → global default → ENV. Fallback graceful a 3 livelli se il modello non è disponibile.

Ingestione documento

Frontend dropzone → POST /api/documents (server, multer)
  → server salva Document(status=pending) + forwarda file a collector
  → collector: parse → chunk → embed → store(vector) → ritorna chunks con text
  → collector callback PUT /api/documents/:id/status { status, chunkCount }
  → server scrive document_chunks + popola searchVector (tsvector) lato DB
  → frontend polla stato fino a completed

Widget chat (visitatore anonimo)

Pagina host → <script src="/widget/:id.js"> (loader)
  → iframe sandboxed → Preact app
  → POST /api/sessions (widget) → server crea WidgetSession (256-bit token, 24h)
  → GET /api/config/:widgetId (branding, trigger, lead)
  → user messaggio → POST /api/chat/:widgetId/stream (widget, x-session-token)
     → sessionMiddleware valida → pre-search RAG (whitelist workspaces, IDOR-safe)
     → proxy a server /api/workspaces/:id/chat/stream (X-Api-Key)
     → relay SSE byte verbatim → Preact parse token/citations/done/error

Boundary e regole architetturali

  1. shared è leaf — nessun import verso altri package, nemmeno import type.
  2. Server ↔ collector = HTTP only — nessun import runtime cross-package; il collector non ha accesso diretto al DB. Permette di scalare il collector su nodi separati.
  3. Widget = servizio separato — non condivide codice con il frontend React; usa Preact per bundle piccolo in iframe sandboxed.
  4. Prisma singleton — importa sempre da src/utils/prisma, mai new PrismaClient().
  5. Soft delete universale — entità cancellabili usano deletedAt; tutte le query filtrano where: { deletedAt: null }.
  6. Config resolutionALWAYS_READONLY (JWT_SECRET, DATABASE_URL, porte, URL): ENV > Default. Tutte le altre chiavi: DB > ENV > Default (modificabili da UI).

Estendere l'architettura

  • Nuova entità/endpoint → aggiungi schema Zod in shared, modello Prisma, route in server/src/routes/, hook TanStack in frontend/src/queries/.
  • Nuova skill agent → registra in server/src/agent/builtinSkills.ts (sandboxed, no esecuzione arbitraria di codice).
  • Nuova lingua → aggiungi frontend/src/i18n/<lang>/translation.json + aggiorna i18n-check.cjs (vedi 10 — Development).
  • Nuovo provider LLM → estendi providerTypeSchema in shared + handler in server/src/agent/llmStreaming.ts + providerService.

Cross-link