Back to docs
Guide 02

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…

6 min read

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

text
┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│   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 via z.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 callback PUT /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)

text
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) | error

Model resolution: per-chat override → workspace default → global default → ENV. Graceful 3-level fallback when the model is unavailable.

Document ingestion

text
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 completed

Widget chat (anonymous visitor)

text
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/error

Job 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)

text
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

  1. `shared` is the leaf - no imports toward other packages, not even import type.
  2. Server ↔ collector = HTTP only - no cross-package runtime imports; the collector has no direct DB access. This allows scaling the collector on separate nodes.
  3. Widget = separate service - it shares no code with the React frontend; it uses Preact for a small bundle in a sandboxed iframe.
  4. Prisma singleton - always import from src/utils/prisma, never new PrismaClient().
  5. Universal soft delete - deletable entities carry deletedAt; every query filters where: { deletedAt: null }.
  6. 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 in server/src/routes/, the TanStack hook in frontend/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 + update i18n-check.cjs (see 10 - Development).
  • New LLM provider → extend providerTypeSchema in shared + handler in server/src/agent/llmStreaming.ts + providerService.

v0.28.1 Provenance

StatementSource
pg-boss: 12 cron-style schedulers + the self-heal queue guard; wiki-consistency in-process; OCR/URL + synthesis 10s pollersdocs/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`).