Arquitectura
Simmetric Chat es un monorepo TypeScript modular con 5 packages más una app de escritorio Tauri. El principio rector es la separación de fronteras: cada package tiene responsabilidades distintas y se comunica con los demás solo mediante interfaces bien d…
Simmetric Chat es un monorepo TypeScript modular con 5 packages más una app de escritorio Tauri. El principio rector es la separación de fronteras: cada package tiene responsabilidades distintas y se comunica con los demás solo mediante interfaces bien definidas (HTTP o el package shared).
Vista general
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 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) │
└──────────────────────────┘Los 5 packages
shared - el leaf node
Tipos, schema Zod y constantes. Cero business logic, única dependencia runtime zod. Lo importan server, collector y frontend. Nunca importar desde otros packages: la dirección es siempre * → shared. Esto garantiza el orden de build de Turborepo y evita ciclos.
src/types/- interfaces de entidades (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). Un schema por dominio, inferencia de tipoz.infer.src/constants/-permissions.ts(31 permisos RBAC),license.ts(11 feature flags enterprise; los 8 flags commodity están siempre activos en Community).
server - el corazón (Express :3000)
API REST + RBAC + orquestador de agente. Ver packages/server/CLAUDE.md.
src/routes/- un archivo por 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(permisos + prevención de IDOR),rateLimit.ts(200/min general, 10/min auth, 10/min probe - ningún limiter de chat dedicado: el agente impone concurrencia/tiempo límite/presupuesto internamente),license.ts(feature flag gating),widgetCors.ts(CORS dinámico por origen).src/services/- authService, providerService (resolución de modelo + capability), hybridSearchService (RRF), ftsService (tsvector), encryptionService (AES-256-GCM), backupSchedulerService (Bree), eventLogService (+ dispatch de webhooks), dlpFilter, etc.src/agent/-orchestrator.ts(bucle ReAct),builtinSkills.ts(rag_search, workspace_memory, document_temp_process),llmStreaming.ts(Ollama/OpenAI/Anthropic),mcpClient.ts(conexiones MCP outbound),mcpServer.ts(RAG como tool MCP para IDE).src/config/env.ts- schema Zod para todas las env var;process.exit(1)si son inválidas.
collector - microservicio de ingesta (Express :3210)
Habla con el servidor solo vía HTTP (secreto COLLECTOR_SECRET). No importa Prisma ni servicios del servidor: frontera crítica para el despliegue air-gap en nodos separados.
src/routes/ingest.ts- upload (multer), query, youtube, delete.src/services/- parser (PDF/DOCX/PPTX/XLSX/TXT/CSV/YouTube), chunker (RecursiveCharacterTextSplitter), embeddings (Xenova local / OpenAI), vectorStore (LanceDB / Qdrant).- Flujo: server → collector
POST /api/ingest/upload→ parse → chunk → embed → store → callback del collectorPUT /api/documents/:id/status→ el servidor actualiza la DB.
widget - chat embebible (Express :3211)
Express CommonJS + bundle Preact IIFE. Se autentica ante el servidor con X-Api-Key.
src/index.ts- factorycreateApp(); static/widget/app.js+ loader routes + API.src/routes/- chat (proxy SSE), session, config, loader, lead.src/widget/- componentes Preact (12 componentes) en un iframe sandboxed.- Proxy SSE transparente: relay de bytes crudos server→iframe, cero parseo.
frontend - SPA (Vite + React 19 :5173)
Nada de Next.js. Proxy /api → server.
- State management 3-tier: TanStack Query (REST/CRUD), React Context (estado UI), fetchEventSource + useState/useRef (SSE, NO TanStack Query).
src/queries/(22 hooks),src/contexts/,src/hooks/useChat.ts(streaming).- i18n: 8 idiomas (en, it, ru, de, fr, es, zh, pt).
src-tauri - app de escritorio
Wrapper Tauri que empaqueta el frontend como app de escritorio; se comunica con el servidor vía HTTP (la misma API). Pensado para despliegue air-gap en máquinas sin navegador.
Data flows principales
Chat streaming (usuario 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) | errorResolución de modelo: override per-chat → workspace default → global default → ENV. Fallback graceful en 3 niveles si el modelo no está disponible.
Ingesta de documentos
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 completedWidget chat (visitante anónimo)
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/errorFronteras y reglas arquitectónicas
- `shared` es leaf - ningún import hacia otros packages, ni siquiera
import type. - Server ↔ collector = HTTP only - ningún import runtime cross-package; el collector no tiene acceso directo a la DB. Permite escalar el collector en nodos separados.
- Widget = servicio separado - no comparte código con el frontend React; usa Preact para un bundle pequeño en iframe sandboxed.
- Prisma singleton - importa siempre desde
src/utils/prisma, nuncanew PrismaClient(). - Soft delete universal - las entidades borrables usan
deletedAt; todas las queries filtranwhere: { deletedAt: null }. - Config resolution -
ALWAYS_READONLY(JWT_SECRET, DATABASE_URL, puertos, URL):ENV > Default. Todas las demás claves:DB > ENV > Default(modificables desde la UI).
Extender la arquitectura
- Nueva entidad/endpoint → añade el schema Zod en
shared, el modelo Prisma, la ruta enserver/src/routes/, el hook TanStack enfrontend/src/queries/. - Nueva skill de agente → regístrala en
server/src/agent/builtinSkills.ts(sandboxed, sin ejecución arbitraria de código). - Nuevo idioma → añade
frontend/src/i18n/<lang>/translation.json+ actualizai18n-check.cjs(ver 10 - Development). - Nuevo provider LLM → extiende
providerTypeSchemaenshared+ handler enserver/src/agent/llmStreaming.ts+providerService.
Cross-link
- Detalles env/config: 03 - Configuration
- Endpoints por dominio: 04 - API Reference
- Internals de features: 05 - Feature Guide