Skip to content

Architecture Overview

lqdflying edited this page Sep 18, 2026 · 11 revisions

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.


Tech Stack

  • 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 via createStyles)
  • 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 bun for scripts, bunx for executables

Runtime Boundary

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.


Repo Layout

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
└── ...

Data Flow

Component → useChatStore action → service (src/services/) → tRPC router → server service → DB

Assistant selection and account transitions

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.

Topic history

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.

Chat completion

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.

MiniMax context trimming

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)".


Store Pattern

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.


tRPC Router Locations

Directory Purpose
src/server/routers/lambda/ Standard request/response
src/server/routers/async/ Long-running / streaming operations
src/server/routers/tools/ Agent tool calls

Path Alias

@/ → src/. In tests: @/database → packages/database/src.


CSS-in-JS

Use createStyles from antd-style. Avoid inline styles or plain CSS modules.


Settings Page Integration

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

Provider Model Options Pipeline

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 panels

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

Clone this wiki locally