Arquitetura
Simmetric Chat é um monorepo TypeScript modular com 5 packages mais um app desktop Tauri. O princípio norteador é a separação de fronteiras: cada package tem responsabilidades distintas e se comunica com os outros apenas por interfaces bem definidas (HTT…
Simmetric Chat é um monorepo TypeScript modular com 5 packages mais um app desktop Tauri. O princípio norteador é a separação de fronteiras: cada package tem responsabilidades distintas e se comunica com os outros apenas por interfaces bem definidas (HTTP ou o package shared).
Visão geral
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 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) │
└──────────────────────────┘Os 5 packages
shared - o leaf node
Tipos, schema Zod e constantes. Zero business logic, única dependência runtime zod. É importado por server, collector, frontend. Nunca importe de outros packages: a direção é sempre * → shared. Isso garante a ordem de build do Turborepo e 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). Um schema por domínio, inferência de tipoz.infer.src/constants/-permissions.ts(31 permissões RBAC),license.ts(11 feature flags enterprise; os 8 flags commodity estão sempre ativos no Community).
server - o coração (Express :3000)
API REST + RBAC + orquestrador de agente. Veja packages/server/CLAUDE.md.
src/routes/- um arquivo por domínio (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(permissões + prevenção de IDOR),rateLimit.ts(200/min geral, 10/min auth, 10/min probe - nenhum limiter de chat dedicado: o agente impõe concorrência/budget/timeout internamente),license.ts(feature flag gating),widgetCors.ts(CORS dinâmico por origem).src/services/- authService, providerService (resolução de modelo + capability), hybridSearchService (RRF), ftsService (tsvector), encryptionService (AES-256-GCM), backupSchedulerService (Bree), eventLogService (+ dispatch de webhook), dlpFilter etc.src/agent/-orchestrator.ts(loop ReAct),builtinSkills.ts(rag_search, workspace_memory, document_temp_process),llmStreaming.ts(Ollama/OpenAI/Anthropic),mcpClient.ts(conexões MCP outbound),mcpServer.ts(RAG como tool MCP para IDE).src/config/env.ts- schema Zod para todas as env var;process.exit(1)se inválidas.
collector - microserviço de ingestão (Express :3210)
Fala com o servidor somente via HTTP (segredo COLLECTOR_SECRET). Não importa Prisma nem serviços do servidor: fronteira crítica para o deploy air-gap em nós 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).- Fluxo: server → collector
POST /api/ingest/upload→ parse → chunk → embed → store → callback do collectorPUT /api/documents/:id/status→ o servidor atualiza o DB.
widget - chat embutível (Express :3211)
Express CommonJS + bundle Preact IIFE. Autenticado no servidor via 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) em iframe sandboxed.- Proxy SSE transparente: relay de bytes crus server→iframe, zero parsing.
frontend - SPA (Vite + React 19 :5173)
Nada de Next.js. Proxy /api → server.
- State management 3-tier: TanStack Query (REST/CRUD), React Context (estado de UI), fetchEventSource + useState/useRef (SSE, NÃO 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 desktop
Wrapper Tauri que empacota o frontend como app desktop; comunica com o servidor via HTTP (mesma API). Pensado para deploy air-gap em máquinas sem navegador.
Data flows principais
Chat streaming (usuário 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) | errorResolução de modelo: override per-chat → workspace default → global default → ENV. Fallback graceful em 3 níveis se o modelo não estiver disponível.
Ingestão 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/errorFronteiras e regras arquiteturais
- `shared` é leaf - nenhum import para outros packages, nem
import type. - Server ↔ collector = apenas HTTP - nenhum import runtime cross-package; o collector não tem acesso direto ao DB. Permite escalar o collector em nós separados.
- Widget = serviço separado - não compartilha código com o frontend React; usa Preact para bundle pequeno em iframe sandboxed.
- Prisma singleton - importe sempre de
src/utils/prisma, nuncanew PrismaClient(). - Soft delete universal - entidades removíveis usam
deletedAt; todas as queries filtramwhere: { deletedAt: null }. - Config resolution -
ALWAYS_READONLY(JWT_SECRET, DATABASE_URL, portas, URL):ENV > Default. Todas as outras chaves:DB > ENV > Default(modificáveis pela UI).
Estender a arquitetura
- Nova entidade/endpoint → adicione o schema Zod em
shared, o modelo Prisma, a rota emserver/src/routes/, o hook TanStack emfrontend/src/queries/. - Nova skill de agente → registre em
server/src/agent/builtinSkills.ts(sandboxed, sem execução arbitrária de código). - Novo idioma → adicione
frontend/src/i18n/<lang>/translation.json+ atualizei18n-check.cjs(veja 10 - Development). - Novo provider LLM → estenda
providerTypeSchemaemshared+ handler emserver/src/agent/llmStreaming.ts+providerService.
Cross-link
- Detalhes de env/config: 03 - Configuration
- Endpoints por domínio: 04 - API Reference
- Internals das features: 05 - Feature Guide