Browser-based management interface for LibreChat. Connects to the same database as the main application and provides a GUI for configuration, user/group/role management, and capability grants.
- Framework: TanStack Start (React 19 + TanStack Router + React Query)
- UI: ClickHouse click-ui component library + Tailwind CSS 4
- Language: TypeScript (strict mode,
verbatimModuleSyntax) - Build: Vite 8
- Testing: Vitest (unit), Playwright (e2e)
- Linting: ESLint
- Package manager: Bun (preferred), pnpm, or npm all work
src/
├── components/
│ ├── access/ # Roles, groups, members management
│ ├── configuration/ # Config editor (schema-driven forms)
│ │ └── fields/ # Individual field type renderers
│ ├── grants/ # Capability grants and audit log
│ ├── shared/ # Reusable UI components
│ └── users/ # User management
├── contexts/ # React contexts (theme)
├── hooks/ # Custom hooks
├── locales/ # i18n translation files
├── routes/ # TanStack Router file-based routes
├── server/ # Server functions (TanStack Start createServerFn)
├── test/ # Test fixtures and setup
├── types/ # TypeScript type definitions
└── utils/ # Pure utility functions
- Single-word file names whenever possible (e.g.,
permissions.ts,capabilities.ts,service.ts). - When multiple words are needed, prefer grouping related modules under a single-word directory rather than using multi-word file names (e.g.,
admin/capabilities.tsnotadminCapabilities.ts). - The directory already provides context —
app/service.tsnotapp/appConfigService.ts.
- Never-nesting: early returns, flat code, minimal indentation. Break complex operations into well-named helpers.
- Functional first: pure functions, immutable data,
map/filter/reduceover imperative loops. Only reach for OOP when it clearly improves domain modeling or state encapsulation. - No dynamic imports unless absolutely necessary.
- Extract repeated logic into utility functions.
- Reusable hooks / higher-order components for UI patterns.
- Parameterized helpers instead of near-duplicate functions.
- Constants for repeated values; configuration objects over duplicated init code.
- Shared validators, centralized error handling, single source of truth for business rules.
- Shared typing system with interfaces/types extending common base definitions.
- Abstraction layers for external API interactions.
- Minimize looping — every additional pass adds up at scale.
- Consolidate sequential O(n) operations into a single pass whenever possible; never loop over the same collection twice if the work can be combined.
- Choose data structures that reduce the need to iterate (e.g.,
Map/Setfor lookups instead ofArray.find/Array.includes). - Avoid unnecessary object creation; consider space-time tradeoffs.
- Prevent memory leaks: careful with closures, dispose resources/event listeners, no circular references.
- Never use
any. Explicit types for all parameters, return values, and variables. - Limit
unknown— avoidunknown,Record<string, unknown>, andas unknown as Tassertions. ARecord<string, unknown>almost always signals a missing explicit type definition. - Don't duplicate types — before defining a new type, check whether it already exists in the project. Reuse and extend existing types rather than creating redundant definitions.
- Use union types, generics, and interfaces appropriately.
- All TypeScript and ESLint warnings/errors must be addressed — do not leave unresolved diagnostics.
- Write self-documenting code; no inline comments narrating what code does.
- JSDoc only for complex/non-obvious logic or intellisense on public APIs.
- Single-line JSDoc for brief docs, multi-line for complex cases.
- Avoid standalone
//comments unless absolutely necessary.
- Limit looping as much as possible. Prefer single-pass transformations and avoid re-iterating the same data.
for (let i = 0; ...)for performance-critical or index-dependent operations.for...offor simple array iteration.for...inonly for object property enumeration.
Imports form a single block with no blank lines, sorted in four groups:
- External packages — shortest line to longest
import typefrom packages — longest line to shortestimport typefrom local modules — longest line to shortest- Local modules — longest line to shortest
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Button, Icon } from '@clickhouse/click-ui';
import type { DynamicSettingProps } from 'librechat-data-provider';
import type { AdminGroup } from '@librechat/data-schemas';
import type * as t from '@/types';
import { SystemCapabilities, getScopeTypeConfig } from '@/constants';
import { baseConfigOptions, saveBaseConfigFn } from '@/server';
import { useLocalize, useCapabilities } from '@/hooks';
import { ScopeSelector } from './ScopeSelector';
import { cn, formatJson } from '@/utils';@tanstack/start-server-coreis banned. Use@tanstack/react-start/serverfor server utilities likegetRequestHeader. This is enforced by ESLintno-restricted-imports.- Always standalone
import type: Type-only imports always use a separateimport typestatement, never inlinetypequalifiers. When a module exports both values and types, use two import statements:
import { PrincipalType } from 'librechat-data-provider';
import type { TCustomConfig } from 'librechat-data-provider';- Namespace imports for local types: Use
import type * as t from '@/types'for local type imports and reference ast.ConfigValue,t.SchemaField, etc. This keeps type usage visually distinct from runtime values and avoids long named import lists:
// PREFERRED
import type * as t from '@/types';
const x: t.ConfigValue = ...;
// AVOID — verbose named imports for types
import type { ConfigValue, SchemaField, FlatConfigMap, ConfigScope } from '@/types';- Barrel imports: Use the barrel for local modules — never import from sub-paths:
@/typesfor all local TypeScript interfaces/type aliases@/constantsfor domain constants, enums re-exported from packages, and derived utilities (getScopeTypeConfig,defaultPermissions, etc.)@/hooksfor all custom hooks@/utilsforcn,formatJson,getInitials, etc.@/components/sharedfor shared UI components
- Barrel imports for
@/serverand@/components: Import from the barrel path, not from sub-files (exception:@/server/sessionand@/server/utils/apiare server-only and excluded from the barrel):
// RIGHT
import { saveConfigFn } from '@/server';
import { ConfigSection } from '@/components/configuration';
// WRONG — importing from sub-files when a barrel exists
import { saveConfigFn } from '@/server/config';
import { ConfigSection } from '@/components/configuration/ConfigSection';- Types live in
src/types/: All locally-defined interfaces and type aliases belong insrc/types/and are imported via@/types. Do not scatter type definitions insidesrc/constants/,src/hooks/, or component files — if a type is shared or reusable, it goes insrc/types/. - Prefer package types over local duplicates: Before defining a new type locally, check
@librechat/data-schemasandlibrechat-data-providerfirst. The goal is little-to-no local duplication of types that already exist in those packages. Local types insrc/types/should only cover domain concepts genuinely specific to this admin panel. - Package types used directly: Types from
@librechat/data-schemasandlibrechat-data-providerare imported directly from those packages, not re-exported through local barrels. Do not recreate locally what the packages already export. - Path alias:
@/maps to./src/*— use for all non-relative imports - Relative imports: Only for sibling/child files in the same feature directory
- Named exports only: No default exports.
The main barrel (@librechat/data-schemas) pulls in Node.js-only modules (async_hooks, winston, etc.) and must never be imported for runtime values in client-side code — it will break the Vite client build.
| What | Where to import from |
|---|---|
Capability constants (SystemCapabilities, CapabilityImplications, etc.) |
@/constants (re-exports from @librechat/data-schemas/capabilities) |
Any import type |
@librechat/data-schemas directly — TypeScript erases these at build time |
Server-only utilities (AppService, etc.) |
Import directly in the src/server/ file that uses them — not through src/server/constants.ts, so TanStack Start's tss-serverfn-split plugin can tree-shake them from the client bundle |
@librechat/data-schemas/capabilities is a clean subpath export with no Node.js deps. src/server/constants.ts re-exports from it for use across server functions.
cn()utility for conditional class merging (wrapsclsx)useLocalize()hook for all user-facing strings (i18n)- Server functions use TanStack Start
createServerFnwith Zod validation - No formatting concerns: Prettier/ESLint formatting is handled separately
- Use
@clickhouse/click-uicomponents (Button,Icon,Dialog,TextField, etc.) wherever possible instead of raw HTML elements. This ensures consistent styling and theming.
bun install
bun run dev # starts dev server on port 3000
bun run build # production build
bun run start # serves production build on port 3000 (requires SESSION_SECRET)
bun run test # vitest unit tests
bun run test:e2e # playwright e2e testsCopy .env.example to .env and fill in the required values. See the file for
all available options. In development (bun run dev), SESSION_SECRET is
optional — a hardcoded dev secret is used automatically.
- Framework: Jest, run per-workspace.
- Run tests from their workspace directory:
cd api && npx jest <pattern>,cd packages/api && npx jest <pattern>, etc. - Frontend tests:
__tests__directories alongside components; usetest/layout-test-utilsfor rendering. - Cover loading, success, and error states for UI/data flows.
- Real logic over mocks. Exercise actual code paths with real dependencies. Mocking is a last resort.
- Spies over mocks. Assert that real functions are called with expected arguments and frequency without replacing underlying logic.
- MongoDB: use
mongodb-memory-serverfor a real in-memory MongoDB instance. Test actual queries and schema validation, not mocked DB calls. - MCP: use real
@modelcontextprotocol/sdkexports for servers, transports, and tool definitions. Mirror real scenarios, don't stub SDK internals. - Only mock what you cannot control: external HTTP APIs, rate-limited services, non-deterministic system calls.
- Heavy mocking is a code smell, not a testing strategy.
Fix all formatting lint errors (trailing spaces, tabs, newlines, indentation) using auto-fix when available. All TypeScript/ESLint warnings and errors must be resolved.
- Schema-driven config editor: The configuration UI is generated from a Zod schema tree. New fields in the schema appear automatically without UI changes.
- Scope-based overrides: Configuration values can be overridden per role or group via "profiles" with priority-based cascading.
- Server function stubs:
users.ts,roles.ts,groups.ts, andcapabilities.tscontain no-op stubs with// TODO: apiFetch(...)comments. Will be wired to the LibreChat Admin API once endpoints are available. SystemCapabilities(from@librechat/data-schemas) for capability constants — not a local enum.