Lobe Chat Project Structure: A Deep Dive into the "Roots vs Features" Architecture
Lobe Chat organizes its codebase using a "roots vs features" architecture that separates thin routing entry points from domain-specific business logic, utilizing Next.js 16 for the web layer, Electron for desktop, Zustand for state management, and a monorepo layout with shared packages.
Lobe Chat is a modern AI chat application built as a Next.js 16 and Electron workspace. Understanding its project structure is essential for contributors extending its functionality. According to the lobehub/lobe-chat source code, the codebase follows a deliberate "roots vs features" split—where src/routes/ holds only page segments while business logic and UI live in src/features/—maximizing modularity and maintainability.
Overview of the "Roots vs Features" Architecture
The project employs a strict two-layer separation that keeps routing concerns distinct from implementation details:
- Roots (Entry Points): Thin wrappers in
src/app/(Next.js app router) andsrc/routes/(SPA page segments) that handle URL mapping and layout composition without containing business logic. - Features (Domain Modules): Self-contained modules under
src/features/that encapsulate UI components, hooks, and local state for specific domains like chat, agents, or settings.
This separation is documented in the built-in spa-routes skill at /.agents/skills/spa-routes/SKILL.md, which states: "This project uses a roots vs features split: src/routes/ only holds page segments; business logic and UI live in src/features/ by domain."
Directory Layout Breakdown
The repository follows a monorepo structure with clear boundaries between the web application, desktop client, and shared libraries:
lobe-chat/
├─ apps/
│ └─ desktop/ # Electron app (main, preload, resources)
├─ packages/
│ ├─ database/ # Drizzle ORM schemas & repositories
│ ├─ agent-runtime/ # Agent execution engine
│ └─ … # Other shared libs
├─ src/
│ ├─ app/ # Next.js 16 app router (pages, layouts, API routes)
│ ├─ routes/ # SPA route "segments" (thin wrappers)
│ ├─ features/ # Domain-oriented UI components & hooks
│ │ ├─ chat/ # Chat UI, message list, input box
│ │ ├─ agent/ # Agent management UI
│ │ └─ … # Other feature folders
│ ├─ store/ # Zustand stores (video, settings, user)
│ ├─ services/ # Client-side services (auth, analytics)
│ ├─ server/ # Server-side utilities (middleware, version check)
│ ├─ layout/ # Global layout providers (theme, i18n, auth)
│ └─ utils/ # Generic helpers (router, locale, file utils)
├─ public/ # Static files (videos, images, favicon)
├─ locales/ # Translation JSON files (zh-TW, en, …)
├─ .github/ # CI/CD workflows, issue templates
├─ next.config.ts # Next.js configuration
└─ vite.config.ts # Vite configuration for SPA parts
Key Architectural Patterns
Routing Layer Separation
The entry points remain intentionally thin. The src/app/ directory contains Next.js pages, API routes, and the global layout that wraps the entire web application. Meanwhile, src/routes/ defines page "segments" (e.g., src/routes/chat/page.tsx) that act as pure delegation layers, importing their actual UI from feature modules.
Feature Module Organization
Each domain maintains its own folder under src/features/. A typical feature module exports:
- UI components (e.g.,
Component.tsx) - Custom hooks (e.g.,
useFeature.ts) - Local store slices when needed
This structure prevents router files from becoming bloated and allows developers to add new capabilities without modifying routing logic.
State Management with Zustand
Global application state lives under src/store/, implemented using Zustand stores. For example, src/store/video/store.ts manages video-related state, while src/store/settings/ handles configuration. These stores are composed and initialized in src/layout/GlobalProvider/StoreInitialization.tsx, ensuring consistent state hydration across the application.
Monorepo Shared Packages
Reusable code that crosses application boundaries resides in packages/:
packages/database/provides Drizzle ORM schemas and repositoriespackages/agent-runtime/contains the agent execution engine- Additional libraries for web crawling, AI providers, etc.
These packages are referenced via the monorepo's pnpm workspace configuration, enabling a single source of truth for data models and business logic across web and desktop builds.
Desktop Integration
The Electron application lives in apps/desktop/, containing the main process (src/main/), preload scripts (src/preload/), and desktop-specific resources. This structure allows the desktop client to share utilities and state management logic with the web codebase while maintaining platform-specific entry points.
Practical Code Examples
Importing Feature Components in Routes
Route files act as thin wrappers that delegate rendering to feature modules:
// src/routes/chat/page.tsx
import ChatPage from '@/features/chat/ChatPage';
export default function Page() {
return <ChatPage />;
}
(Source: [src/routes/chat/page.tsx](https://github.com/lobehub/lobe-chat/blob/canary/src/routes/chat/page.tsx))
Consuming Global State
Feature components access shared state through Zustand hooks:
// src/features/video/VideoPlayer.tsx
import { useVideoStore } from '@/store/video/store';
export default function VideoPlayer() {
const { currentVideo, play } = useVideoStore(state => ({
currentVideo: state.currentVideo,
play: state.play,
}));
return <video src={currentVideo} onPlay={play} controls />;
}
(Source: [src/store/video/store.ts](https://github.com/lobehub/lobe-chat/blob/canary/src/store/video/store.ts))
Creating a New Feature Module
To add a new domain feature:
# 1️⃣ Create folder
mkdir -p src/features/notes
# 2️⃣ Add component
cat > src/features/notes/NotesPanel.tsx <<'EOF'
import { useNotesStore } from '@/store/notes';
export default function NotesPanel() {
const notes = useNotesStore(s => s.list);
return <div>{notes.map(n => <p key={n.id}>{n.content}</p>)}</div>;
}
EOF
# 3️⃣ Export from feature index
echo "export { default as NotesPanel } from './NotesPanel';" > src/features/notes/index.ts
The new panel can then be imported anywhere in the application:
import { NotesPanel } from '@/features/notes';
Accessing Shared Packages
Database models and utilities are imported from the monorepo packages:
import { db } from '@lobehub/database';
import { ChatMessage } from '@lobehub/database/models';
async function saveMessage(msg: ChatMessage) {
await db.insert(ChatMessage).values(msg);
}
(Source: [packages/database/README.md](https://github.com/lobehub/lobe-chat/blob/canary/packages/database/README.md))
Critical Files and Their Roles
Several configuration and provider files serve as the backbone of the architecture:
next.config.ts: Configures Next.js with rewrites, image domains, and i18n settings.vite.config.ts: Sets up Vite for SPA parts, handling SSR and path aliasing.src/layout/GlobalProvider/AppTheme.tsx: Provides Ant Design theme context and dark-mode handling.src/layout/GlobalProvider/StoreInitialization.tsx: Initializes all Zustand stores when the application starts.src/features/*/index.ts: Public re-export barrels for each feature module, establishing clean import contracts.apps/desktop/main/index.ts: Electron main process entry point managing window creation and IPC communication.packages/database/src/index.ts: Exposes database models and utilities to the entire monorepo.
Summary
- Lobe Chat uses a "roots vs features" split where
src/routes/contains only thin page segments andsrc/features/holds domain-specific business logic. - Next.js 16 powers the web layer in
src/app/, while Electron lives inapps/desktop/. - Zustand stores in
src/store/manage global state, initialized viaStoreInitialization.tsx. - Monorepo packages in
packages/share database schemas, agent runtimes, and utilities across applications. - Route files delegate rendering to feature modules, keeping routing logic decoupled from UI implementation.
Frequently Asked Questions
What is the difference between src/app/ and src/routes/ in Lobe Chat?
src/app/ contains the Next.js 16 app router pages, API routes, and global layouts that wrap the entire application. src/routes/ serves as the SPA route segment layer, holding thin page wrappers that import their actual UI components from src/features/. This separation ensures routing files remain lightweight while complex business logic resides in dedicated feature modules.
How does Lobe Chat manage global state across the application?
The project uses Zustand for state management, with individual stores organized under src/store/ by domain (e.g., src/store/video/store.ts, src/store/settings/). These stores are composed and initialized in src/layout/GlobalProvider/StoreInitialization.tsx, ensuring consistent state availability throughout the component tree without prop drilling.
Where should new features be added in the Lobe Chat codebase?
New capabilities belong in src/features/ under a domain-specific folder (e.g., src/features/notes/). Each feature should export its components and hooks through an index.ts barrel file. The feature can then be imported into route files in src/routes/ or src/app/ without modifying the router's structural logic, maintaining the "roots vs features" architectural boundary.
How are database models shared between the web and desktop applications?
Database schemas and repositories live in packages/database/ as part of the monorepo structure. Both the Next.js application and Electron desktop client import these models using the @lobehub/database workspace alias. This setup ensures data consistency across platforms while avoiding code duplication between the web and desktop builds.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →