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) and src/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:

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 repositories
  • packages/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:

Summary

  • Lobe Chat uses a "roots vs features" split where src/routes/ contains only thin page segments and src/features/ holds domain-specific business logic.
  • Next.js 16 powers the web layer in src/app/, while Electron lives in apps/desktop/.
  • Zustand stores in src/store/ manage global state, initialized via StoreInitialization.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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →