9.9 KiB
Role-Based Configuration Architecture
This document outlines the foundational architecture for role-based configuration management in LibreChat.
Overview
The new architecture extends LibreChat's configuration system to support role, group, and user-specific configuration overrides while maintaining the existing librechat.yaml as the base configuration. The system uses a priority-based merge strategy where configurations are applied in order from lowest to highest priority.
Architecture Components
1. Database Schema
A single unified MongoDB collection following the ACL pattern:
Config
- Purpose: Store configurations for any principal (user, group, or role)
- Key Fields:
principalType: Type of entity (PrincipalType.USER,PrincipalType.GROUP,PrincipalType.ROLE)principalId: ID of the entity (ObjectId for users/groups, string for roles)principalModel: Model reference (PrincipalModel.USER,PrincipalModel.GROUP,PrincipalModel.ROLE)priority: Priority level for merge order (higher = more specific)overrides: Object matchinglibrechat.yamlstructureisActive: Toggle to enable/disableconfigVersion: Auto-increments for cache invalidation
Location:
- Type:
packages/data-schemas/src/types/config.ts - Schema:
packages/data-schemas/src/schema/config.ts - Model:
packages/data-schemas/src/models/config.ts
Design: This follows LibreChat's existing ACL pattern (AclEntry), using a single collection for all principal types rather than separate collections per type.
2. Database Methods
File: packages/data-schemas/src/methods/config.ts (exported via @librechat/data-schemas)
Database operations for config management:
Key Methods:
-
getApplicableConfigs(principals): Fetches all configs for given principals- Takes array of principals from
getUserPrincipals - Single optimized
$orquery to DB - Returns array of
IConfigdocuments
- Takes array of principals from
-
findConfigByPrincipal(principalType, principalId): Find config for specific principal -
upsertConfig(...): Create or update a config -
deleteConfig(...): Delete a config -
toggleConfigActive(...): Enable/disable a config
3. Configuration Resolution Service
File: packages/api/src/config/resolution.ts (exported via @librechat/api)
Simple service for merging configurations:
Key Functions:
-
buildUserConfig({ baseConfig, cachedConfigs }): Merges base config with overrides- Takes fresh
baseConfigfrom YAML - Takes cached
IConfig[]documents - Returns merged
TCustomConfig - Uses
deepmergenpm package for merging
- Takes fresh
-
mergeConfigsFromDB(baseConfig, configs): Internal helper- Sorts configs by priority (ascending)
- Merges each config's overrides in order
4. Updated getAppConfig
File: api/server/services/Config/app.js
The main config accessor with granular caching:
userIdorrole: Determines which configs to apply- Granular caching: Each config cached individually by principal
- Cache keys:
config:{principalType}:{principalId} - Graceful fallback to base config on errors
Configuration Priority System
Configurations are applied in order from lowest to highest priority:
Priority 0: Base YAML config (librechat.yaml)
Priority X: All Config entries sorted by priority field
Important:
- Priority values are not hardcoded - each Config has its own priority
- Suggested defaults: Role (10-30), Group (20-50), User (100+)
- Higher priority values always override lower ones
- If a user belongs to multiple groups, each group config is applied in priority order
How It Works
Request Flow
- Request comes in with
userIdand/orrole - Load base config from
librechat.yaml(cached asBASE_CONFIG_KEY) - Get user principals via
getUserPrincipals({ userId, role, includeGroups: false })- Returns array: user, role (no groups for initial implementation)
- Check cache individually for each principal:
config:role:admin→ admin role configconfig:user:123→ user 123's config
- If any cache miss:
- Single optimized DB query fetches all missing configs
- Cache each returned config individually
- Merge fresh baseConfig with all configs (cached + freshly fetched)
- Return the final merged config
Cache Strategy
Granular individual caching:
BASE_CONFIG_KEY: The full processed AppConfig from YAMLconfig:role:{roleName}: Role config from DB (shared across all users with that role)config:user:{userId}: User-specific config from DB
Benefits:
- Efficient: Role configs shared across users (e.g., all admins share
config:role:admin) - Granular invalidation: Change role config? Only clear that one cache entry
- YAML independent: Base config changes don't affect config caches
- Optimized queries: Single DB query for all missing configs
Example Scenario
// User "alice" has:
// - Role: "developer"
// Config: { principalType: 'role', principalId: 'developer', priority: 10 }
// - Groups: ["engineering-team", "beta-testers"]
// Configs: [
// { principalType: 'group', principalId: ObjectId(engineering-team), priority: 25 },
// { principalType: 'group', principalId: ObjectId(beta-testers), priority: 30 }
// ]
// - User config:
// Config: { principalType: 'user', principalId: ObjectId(alice), priority: 100 }
// Merge order:
// 1. Base YAML (priority 0)
// 2. Role "developer" config (priority 10)
// 3. Group "engineering-team" config (priority 25)
// 4. Group "beta-testers" config (priority 30)
// 5. User "alice" config (priority 100)
Integration Points
Current Integration
- Uses existing ACL system for group membership (
getUserGroups) - Maintains existing role system (1:1 user-role relationship)
- Preserves existing cache infrastructure (Redis/in-memory)
- Compatible with current
librechat.yamlstructure
What's NOT Yet Implemented
This is the foundation only. Still needed:
- Admin UI: Interface to create/edit configs
- API Endpoints: REST API for CRUD operations on configs
- Validation: Schema validation for config objects
- Cache Invalidation: Pub/sub for multi-instance cache invalidation
- Testing: Integration and unit tests
- Migration Tools: Scripts to help migrate existing configs
- Documentation: Admin guide for managing configs
- Audit Logging: Track who changed what configs when
Usage Example
// In a request handler
const { getAppConfig } = require('~/server/services/Config/app');
// Get config for specific user
const config = await getAppConfig({ userId: req.user.id });
// Config will include all applicable overrides merged in priority order
console.log(config.endpoints); // User's effective endpoint config
File Structure
packages/data-schemas/src/
├── types/
│ └── config.ts # IConfig type definitions
├── schema/
│ └── config.ts # Config Mongoose schema
├── models/
│ └── config.ts # Config model factory
└── methods/
└── config.ts # Config DB operations
packages/api/src/
├── config/
│ └── resolution.ts # Config resolution service (uses TCustomConfig)
└── types/
└── config.ts # AppConfig type
api/server/services/Config/
└── app.js # Updated getAppConfig (main entry point)
Note: Uses deepmerge npm package for merging (not custom implementation)
Next Steps
To build on this foundation, consider implementing in this order:
- Create API endpoints for managing configs (CRUD)
- Add validation to ensure config objects match expected structure
- Build simple CLI tools to test config creation/assignment
- Add cache invalidation logic (pub/sub for Redis)
- Create admin UI for visual config management
- Write tests for the resolution logic
- Document the config format and best practices
Design Decisions
Why Priority-Based?
- Predictable: Always know which config wins
- Flexible: Can adjust priority for special cases
- Scalable: Works with complex org hierarchies
Why Single Schema (ACL Pattern)?
- Consistency: Matches existing LibreChat ACL architecture
- Simplicity: One collection instead of three
- Flexibility: Easy to add new principal types in the future
- Queries: Simple to fetch all overrides for any principal
Why MongoDB?
- Flexible Schema:
overridescan match any YAML structure - Already in use: No new dependencies
- ACL Integration: Leverages existing group/role system
Why Granular Caching?
- Individual cache entries: Each config cached by
config:{type}:{id} - Efficient sharing: All admins share
config:role:admincache - Granular invalidation: Change one config? Clear only that cache entry
- YAML independence: Base config changes don't invalidate config caches
- Optimized queries: Single DB query for all cache misses
- Fresh merges: Each request merges fresh baseConfig with cached overrides
Type Safety & Architecture
- DB Layer (
data-schemas): UsesIConfiginterface for DB documents - App Layer (
api): UsesTCustomConfigfromlibrechat-data-providerfor type safety - No
anytypes: All types properly defined and enforced - Separation of Concerns: DB methods in data-schemas, business logic in api
- Optimized Queries: Uses
getUserPrincipalsto batch principal lookup - Smart Caching: Caches raw DB overrides separately from base config
Notes
- The
overridesfield usesSchema.Types.Mixedto support any structure matchinglibrechat.yaml - Typed as
Partial<TCustomConfig>at the app layer for type safety - Priority values are just defaults - they can be customized per config
- All configs have
isActiveflag for easy enable/disable without deletion configVersionauto-increments on changes to help with cache invalidation