Architecture
Simmetric Chat is a TypeScript monorepo with 5 packages plus a Tauri desktop app, organized by strict boundary separation: every package owns distinct responsibilities and talks to the others only through well-defined interfaces (HTTP or the shared types…
Simmetric Chat is a TypeScript monorepo with 5 packages plus a Tauri desktop app, organized by strict boundary separation: every package owns distinct responsibilities and talks to the others only through well-defined interfaces (HTTP or the shared types package).
Overview
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ frontend │ │ widget │ │ src-tauri │
│ React 19 │ │ Preact IIFE │ │ Desktop app │
│ Vite :5173 │ │ Express:3211│ │ │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ /api (JWT) │ /api/internal/widget (X-Api-Key) │
│ │ │
▼ ▼ │
┌────────────────────────────────────┐ │
│ server │◄──── Tauri uses ─────┘
│ Express :3000 (RBAC, agent, RAG) │ the server via HTTP
└──────┬──────────────────┬──────────┘
│ HTTP (COLLECTOR_SECRET) │ HTTP (status callback)
▼ ▼
┌──────────────┐ ┌──────────────┐
│ collector │ │ PostgreSQL │
│ Express:3210│ │ (Prisma) │
│ parse/embed │ └──────────────┘
└──────────────┘
│
▼
┌──────────────────────────┐
│ Vector DB (LanceDB/ │
│ Qdrant) + LLM (Ollama/ │
│ OpenAI/Anthropic) │
└──────────────────────────┘The 5 packages
shared - the leaf node
Types, Zod schemas and constants. Zero business logic, single runtime dependency zod. It is imported by server, collector, frontend. Never import from the other packages: the direction is always * → shared. This guarantees the Turborepo build order and prevents cycles.
src/types/- entity interfaces (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). One schema per domain, type inference viaz.infer.src/constants/-permissions.ts(31 RBAC permissions),license.ts(11 enterprise feature flags; the 8 commodity flags are always on in Community).
server - the core (Express :3000)
REST API + RBAC + agent orchestrator. See packages/server/CLAUDE.md.
src/routes/- one file per domain (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(permissions + IDOR prevention),rateLimit.ts(200/min general, 10/min auth, 10/min probe - no dedicated chat limiter: the agent enforces concurrency/budget/timeout internally),license.ts(feature flag gating),widgetCors.ts(dynamic per-origin CORS).src/services/- authService, providerService (model resolution + capability), hybridSearchService (RRF), ftsService (tsvector), encryptionService (AES-256-GCM), backupSchedulerService (Bree), eventLogService (+ webhook dispatch), dlpFilter, etc.src/agent/-orchestrator.ts(ReAct loop),builtinSkills.ts(rag_search, workspace_memory, document_temp_process),llmStreaming.ts(Ollama/OpenAI/Anthropic),mcpClient.ts(outbound MCP connections),mcpServer.ts(RAG as an MCP tool for IDEs).src/config/env.ts- Zod schema for all env vars;process.exit(1)when invalid.
collector - ingestion microservice (Express :3210)
It talks to the server only over HTTP (shared secret COLLECTOR_SECRET). It never imports Prisma or server services: a critical boundary for air-gap deployment on separate nodes.
src/routes/ingest.ts- upload (multer), query, youtube, delete.src/services/- parser (PDF/DOCX/PPTX/XLSX/TXT/CSV/YouTube), chunker (RecursiveCharacterTextSplitter), embeddings (local Xenova / OpenAI), vectorStore (LanceDB / Qdrant).- Flow: server → collector
POST /api/ingest/upload→ parse → chunk → embed → store → collector callbackPUT /api/documents/:id/status→ server updates the DB.
widget - embeddable chat (Express :3211)
Express CommonJS + Preact IIFE bundle. Authenticates to the 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/- Preact components (12 components) in a sandboxed iframe.- Transparent SSE proxy: relays raw bytes server→iframe, zero parsing.
frontend - SPA (Vite + React 19 :5173)
No Next.js. Proxy /api → server.
- 3-tier state management: TanStack Query (REST/CRUD), React Context (UI state), fetchEventSource + useState/useRef (SSE, NOT TanStack Query).
src/queries/(22 hooks),src/contexts/,src/hooks/useChat.ts(streaming).- i18n: 8 locales (en, it, ru, de, fr, es, zh, pt).
src-tauri - desktop app
A Tauri wrapper that packages the frontend as a desktop app; it communicates with the server over HTTP (same API). Designed for air-gap deployment on machines without a browser.
Main data flows
Chat streaming (internal user)
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) when needed
├─ llmStreaming (resolved provider: per-chat → workspace → global → ENV)
└─ tool execution (sandboxed)
→ SSE: token | status | citations | done(modelUsed,providerUsed,mcpSources) | errorModel resolution: per-chat override → workspace default → global default → ENV. Graceful 3-level fallback when the model is unavailable.
Document ingestion
Frontend dropzone → POST /api/documents (server, multer)
→ server saves Document(status=pending) + forwards the file to the collector
→ collector: parse → chunk → embed → store(vector) → returns chunks with text
→ collector callback PUT /api/documents/:id/status { status, chunkCount }
→ server writes document_chunks + populates searchVector (tsvector) DB-side
→ frontend polls the status until completedWidget chat (anonymous visitor)
Host page → <script src="/widget/:id.js"> (loader)
→ sandboxed iframe → Preact app
→ POST /api/sessions (widget) → server creates WidgetSession (256-bit token, 24h)
→ GET /api/config/:widgetId (branding, trigger, lead)
→ user message → POST /api/chat/:widgetId/stream (widget, x-session-token)
→ sessionMiddleware validates → pre-search RAG (whitelisted workspaces, IDOR-safe)
→ proxies to server /api/workspaces/:id/chat/stream (X-Api-Key)
→ SSE relay byte verbatim → Preact parses token/citations/done/errorJob queue & schedulers (pg-boss)
pg-boss (Postgres-backed queue, singleton in services/jobQueue.ts) runs 12 cron-style schedulers: MCP health-check, MCP reaper, synthesis reaper, the weekly fidelity sampling, vector cleanup, upload-draft reaping, chat-message retention, the hourly token-quota reset sweep (services/quotaResetJob.ts), the DLP document scan/backfill, MCP OAuth token refresh, and the daily connector health sweep. A self-heal guard (services/pgbossQueueHealthService.ts) repairs queue-table duplicates before registration. If Postgres is unavailable, getBoss() === null: the schedulers stay offline with a warn - no fallback timers (initWikiConsistencyScheduler runs as an in-process interval, not via pg-boss). The OCR/URL and synthesis pipelines poll every 10 seconds as in-process intervals (dev and production).
Connector inbound message flow (Enterprise)
Platform (Telegram/Discord/Slack/WhatsApp) → PUBLIC webhook router (per-platform signature/secret, no RBAC)
→ messageRouter (archive binding → chat/reply routing; welcome/fallback messages)
→ inbound message + the knowledge-base reply; the EDIT goes through the admin API (F-CONN-2: chat_connector.updated)See 13 - Connectors.
Boundaries and architectural rules
- `shared` is the leaf - no imports toward other packages, not even
import type. - Server ↔ collector = HTTP only - no cross-package runtime imports; the collector has no direct DB access. This allows scaling the collector on separate nodes.
- Widget = separate service - it shares no code with the React frontend; it uses Preact for a small bundle in a sandboxed iframe.
- Prisma singleton - always import from
src/utils/prisma, nevernew PrismaClient(). - Universal soft delete - deletable entities carry
deletedAt; every query filterswhere: { deletedAt: null }. - Config resolution -
ALWAYS_READONLY(JWT_SECRET, DATABASE_URL, ports, URLs):ENV > Default. All other keys:DB > ENV > Default(editable from the UI).
Extending the architecture
- New entity/endpoint → add the Zod schema in
shared, the Prisma model, the route inserver/src/routes/, the TanStack hook infrontend/src/queries/. - New agent skill → register it in
server/src/agent/builtinSkills.ts(sandboxed, no arbitrary code execution). - New language → add
frontend/src/i18n/<lang>/translation.json+ updatei18n-check.cjs(see 10 - Development). - New LLM provider → extend
providerTypeSchemainshared+ handler inserver/src/agent/llmStreaming.ts+providerService.
v0.28.1 Provenance
| Statement | Source |
|---|---|
| pg-boss: 12 cron-style schedulers + the self-heal queue guard; wiki-consistency in-process; OCR/URL + synthesis 10s pollers | docs/ARCHITECTURE.md:175-176 (row-time) |
| The connector inbound flow (public no-RBAC webhook → messageRouter) | packages/server/src/routes/connectors.ts (header comment, row-time) - F-CONN-2 |
Operational reference - Simmetric Chat v0.28.1 (docs updated 2026-10-02, census SHA `aca5b8ff`).
Cross-link
- Env/config details: 03 - Configuration
- Endpoints per domain: 04 - API Reference
- Internal features: 05 - Feature Guide