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 inferencez.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 callbackPUT /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.ts—createApp()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
sharedè leaf — nessun import verso altri package, nemmenoimport type.- 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.
- Widget = servizio separato — non condivide codice con il frontend React; usa Preact per bundle piccolo in iframe sandboxed.
- Prisma singleton — importa sempre da
src/utils/prisma, mainew PrismaClient(). - Soft delete universale — entità cancellabili usano
deletedAt; tutte le query filtranowhere: { deletedAt: null }. - Config resolution —
ALWAYS_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 inserver/src/routes/, hook TanStack infrontend/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+ aggiornai18n-check.cjs(vedi 10 — Development). - Nuovo provider LLM → estendi
providerTypeSchemainshared+ handler inserver/src/agent/llmStreaming.ts+providerService.
Cross-link
- Dettagli env/config: 03 — Configuration
- Endpoint per dominio: 04 — API Reference
- Internals feature: 05 — Feature Guide