Repository navigation
Architecture Overview
ChatHub is a Next.js 15 + React 19 application using a hybrid App Router + SPA approach. It targets self-hosted Docker + PostgreSQL only.
-
Frontend: Next.js 15, React 19, TypeScript -- hybrid Next.js App Router + SPA (
react-router-dom) -
UI:
@lobehub/ui, Ant Design,antd-style(CSS-in-JS viacreateStyles) - State: Zustand stores + SWR for server data
- Backend: tRPC + Next.js API routes
- Database: PostgreSQL only (server mode), Drizzle ORM
- Testing: Vitest + Testing Library (unit), Playwright + Cucumber (e2e)
-
Monorepo: pnpm workspaces; use
bunfor scripts,bunxfor executables
ChatHub has one deployment mode: the Next.js application server backed by PostgreSQL. Browsers, mobile browsers, and installed PWAs all use the same server APIs. On mobile and the installed PWA, the session list stays mounted behind the workspace so Back is instant, without keeping every session row subscribed while you are in a topic; the topic sheet unmounts while closed. ChatHub does not ship an Electron desktop client, a PGlite database in the browser, or the older IndexedDB/Dexie local edition.
Browser localStorage may still hold transient interface preferences, but it is
not an account database. Durable conversations, settings, plugins, and OAuth
state belong to PostgreSQL. Legacy JSON exports can be imported into a current
account; see Data Backup and Restore.
src/
├── app/ # Next.js App Router
│ ├── (backend)/ # Server-only routes (API, tRPC endpoints)
│ └── [variants]/ # Client SPA entry points
│ └── (main)/
│ ├── tools/ # Tools Hub (Picbed, API Tester, etc.)
│ └── ... # chat, discover, image, settings, etc.
├── features/ # Large self-contained UI feature areas (e.g. Conversation/)
├── store/ # Zustand stores (one per domain)
├── services/ # Client-side service layer (calls tRPC/API)
├── server/
│ ├── routers/ # tRPC routers
│ └── services/ # Server-side service implementations
├── locales/default/ # i18n source of truth (TypeScript)
└── libs/ # Shared utilities and abstractions
packages/
├── database/ # Drizzle schemas, models, repositories (@lobechat/database)
├── agent-runtime/ # LLM provider adapters
├── model-bank/ # Model definitions and capabilities
└── ...
Component → useChatStore action → service (src/services/) → tRPC router → server service → DB
Selecting an assistant updates the chat URL first. ChatHub activates that assistant only after it verifies the current account and confirms that the initialized assistant list contains the requested ID. This is especially important on mobile, where restoring the app and loading account state can happen at the same time.
A valid assistant deep link waits through account/session-list loading rather than jumping to Just Chat. If loading finishes and the assistant is not in the current account, ChatHub safely returns to Just Chat. Inbox remains available while identity is unresolved, and an account reset cannot restore an assistant from the previous account through a stale URL update.
If loading the current account fails, ChatHub replaces the default or searched assistant list with one recovery message instead of leaving visible assistants unresponsive. Use Retry for a network/request failure, or Sign in again when the signed-in identity is unresolved or mismatched. Just Chat remains available during recovery. All assistant-creation controls remain hidden until the account is ready, including desktop/mobile headers, the Inbox welcome plus, empty-list add buttons, and group menus. A group-creation wizard that was already open closes when recovery begins and does not reopen automatically. Selecting Just Chat synchronously cancels pending assistant activation before the URL update or any desktop new-topic action, so the assistant cannot activate afterward against the user's choice.
Account-owned settings such as Provider, MCP, Storage, assistant, system assistant, image, and speech settings remain on their loading screen while authentication is still being resolved. Their controls appear only after the account scope is known; account-independent pages such as About remain available.
Each Chat topic row shows its exact last-activity date and time using the viewer's locale and browser timezone. Relative group headings such as This Week and This Month remain available for scanning the history.
src/services/chat/index.ts assembles ChatStreamPayload (messages, tools, extendParams from agent chatConfig + model bank extendParams) for the connected-tab runtime.
Background conversation generation runs the same payload path on the server
worker under src/server/services/conversationGeneration/. Closing a tab does
not cancel that job. See
Background Conversation Generation.
Heavy tool/plugin payloads can exceed MiniMax's effective input budget. trimMinimaxChatContext (src/services/chat/trimMinimaxContext.ts) estimates size for messages + tools, derives a safe input budget from the model card, and drops oldest user/assistant turns until the estimate fits. This reduces bad_request_error / "context window exceeds limit (2013)".
Every Zustand store follows this layout:
store/<domain>/
├── store.ts # createWithEqualityFn, merges slices
├── initialState.ts # Full state type + default values
├── selectors.ts # Derived state selectors
└── slices/ # Action groups (each slice = one concern)
Prefer vi.spyOn over vi.mock when testing stores.
| Directory | Purpose |
|---|---|
src/server/routers/lambda/ |
Standard request/response |
src/server/routers/async/ |
Long-running / streaming operations |
src/server/routers/tools/ |
Agent tool calls |
@/ → src/. In tests: @/database → packages/database/src.
Use createStyles from antd-style. Avoid inline styles or plain CSS modules.
To add a new settings tab:
| Layer | File |
|---|---|
| Enum |
src/store/global/initialState.ts -- add to SettingsTabs enum |
| Sidebar |
src/app/[variants]/(main)/settings/hooks/useCategory.tsx -- add menu item |
| Component map |
src/app/[variants]/(main)/settings/_layout/SettingsContent.tsx -- add dynamic(() => import('...')) entry |
| Page | src/app/[variants]/(main)/settings/<tab>/index.tsx |
| i18n |
src/locales/default/setting.ts + `locales/en-US |
When adding per-model extension toggles (gear menu), follow this pipeline:
| Layer | Location (typical) |
|---|---|
| Catalogue |
packages/model-bank/src/aiModels/<provider>.ts -- settings.extendParams?: ExtendParamsType[] per model id |
| ExtendParams union |
packages/model-bank/src/types/aiModel.ts -- add a new union member only when wiring is real |
| Agent config |
packages/types/src/agent/chatConfig.ts -- field + AgentChatConfigSchema when persisted |
| Request assembly |
src/services/chat/index.ts -- map chatConfig → ChatStreamPayload / extendParams
|
| Provider runtime |
packages/model-runtime/src/providers/<provider>/ -- exact JSON body; unit tests for payload shape |
| UI | Provider-specific panel (see below) + i18n (src/locales/default/chat.ts + locale JSON) |
| Provider id | Component |
|---|---|
moonshot |
providers/MoonshotOptions.tsx |
minimax |
providers/MinimaxOptions.tsx |
anthropic |
providers/AnthropicOptions.tsx |
openai |
providers/OpenAIOptions.tsx |
deepseek |
providers/DeepSeekOptions.tsx |
mimo |
providers/MimoOptions.tsx |
| everything else | ControlsForm.tsx |
- Home
- Quick Start
- Docker Deployment and Upgrades
- Authentication
- Model Providers
- Knowledge Base and RAG
- MarkItDown Sidecar
- Sandbox (OpenSandbox)
- OpenAI Compatible Cache Matrix
- Model Extension Options
- Memory & Context Compaction
- Background Conversation Generation
- Pasted Text
- Context Export
- Data Backup and Restore
- Image Generation
- Artifacts
- Inline SVG Diagrams
- Tools Hub
- MCP OAuth
- MCP Management
- Agent Skills
- Database
- Development
- Debug and Diagnostics
- Architecture Overview
- Docker Release
- Release Workflow