LibreChat/ROLE_BASED_CONFIG.md
2026-01-12 10:16:36 -05:00

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 matching librechat.yaml structure
    • isActive: Toggle to enable/disable
    • configVersion: 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 $or query to DB
    • Returns array of IConfig documents
  • 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 baseConfig from YAML
    • Takes cached IConfig[] documents
    • Returns merged TCustomConfig
    • Uses deepmerge npm package for merging
  • 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:

  • userId or role: 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

  1. Request comes in with userId and/or role
  2. Load base config from librechat.yaml (cached as BASE_CONFIG_KEY)
  3. Get user principals via getUserPrincipals({ userId, role, includeGroups: false })
    • Returns array: user, role (no groups for initial implementation)
  4. Check cache individually for each principal:
    • config:role:admin → admin role config
    • config:user:123 → user 123's config
  5. If any cache miss:
    • Single optimized DB query fetches all missing configs
    • Cache each returned config individually
  6. Merge fresh baseConfig with all configs (cached + freshly fetched)
  7. Return the final merged config

Cache Strategy

Granular individual caching:

  • BASE_CONFIG_KEY: The full processed AppConfig from YAML
  • config: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.yaml structure

What's NOT Yet Implemented

This is the foundation only. Still needed:

  1. Admin UI: Interface to create/edit configs
  2. API Endpoints: REST API for CRUD operations on configs
  3. Validation: Schema validation for config objects
  4. Cache Invalidation: Pub/sub for multi-instance cache invalidation
  5. Testing: Integration and unit tests
  6. Migration Tools: Scripts to help migrate existing configs
  7. Documentation: Admin guide for managing configs
  8. 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:

  1. Create API endpoints for managing configs (CRUD)
  2. Add validation to ensure config objects match expected structure
  3. Build simple CLI tools to test config creation/assignment
  4. Add cache invalidation logic (pub/sub for Redis)
  5. Create admin UI for visual config management
  6. Write tests for the resolution logic
  7. 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: overrides can 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:admin cache
  • 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): Uses IConfig interface for DB documents
  • App Layer (api): Uses TCustomConfig from librechat-data-provider for type safety
  • No any types: All types properly defined and enforced
  • Separation of Concerns: DB methods in data-schemas, business logic in api
  • Optimized Queries: Uses getUserPrincipals to batch principal lookup
  • Smart Caching: Caches raw DB overrides separately from base config

Notes

  • The overrides field uses Schema.Types.Mixed to support any structure matching librechat.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 isActive flag for easy enable/disable without deletion
  • configVersion auto-increments on changes to help with cache invalidation