LibreChat/packages/data-schemas
Danny Avila 428ef2eb15
🏢 feat: Multi-Tenant Data Isolation Infrastructure (#12091)
* chore: imports

* chore: optional chaining in `spendTokens.spec.ts`

* feat: Add tenantId field to all MongoDB schemas for multi-tenant isolation

  - Add AsyncLocalStorage-based tenant context (`tenantContext.ts`) for
    request-scoped tenantId propagation without modifying method signatures
  - Add Mongoose `applyTenantIsolation` plugin that injects `{ tenantId }`
    into all query filters when tenant context is present, with
    `TENANT_ISOLATION_STRICT` env var for fail-closed production mode
  - Add optional `tenantId` field to all 28 collection schemas
  - Update all compound unique indexes to include tenantId (email, OAuth IDs,
    role names, serverName, conversationId+user, messageId+user, etc.)
  - Apply tenant isolation plugin in all 28 model factories
  - Add `tenantId?: string` to all TypeScript document interfaces

  Behaviorally inert — transitional mode (default) passes through all queries
  unchanged. No migration required for existing deployments.

* refactor: Update tenant context and enhance tenant isolation plugin

- Changed `tenantId` in `TenantContext` to be optional, allowing for more flexible usage.
- Refactored `runAsSystem` function to accept synchronous functions, improving usability.
- Introduced comprehensive tests for the `applyTenantIsolation` plugin, ensuring correct tenant filtering in various query scenarios.
- Enhanced the plugin to handle aggregate queries and save operations with tenant context, improving data isolation capabilities.

* docs: tenant context documentation and improve tenant isolation tests

- Added detailed documentation for the `tenantStorage` AsyncLocalStorage instance in `tenantContext.ts`, clarifying its usage for async tenant context propagation.
- Updated tests in `tenantIsolation.spec.ts` to improve clarity and coverage, including new tests for strict mode behavior and tenant context propagation through await boundaries.
- Refactored existing test cases for better readability and consistency, ensuring robust validation of tenant isolation functionality.

* feat: Enhance tenant isolation by preventing tenantId mutations in update operations

- Added a new function to assert that tenantId cannot be modified through update operators in Mongoose queries.
- Implemented middleware to enforce this restriction during findOneAndUpdate, updateOne, and updateMany operations.
- Updated documentation to reflect the new behavior regarding tenantId modifications, ensuring clarity on tenant isolation rules.

* feat: Enhance tenant isolation tests and enforce tenantId restrictions

- Updated existing tests to clarify behavior regarding tenantId preservation during save and insertMany operations.
- Introduced new tests to validate that tenantId cannot be modified through update operations, ensuring strict adherence to tenant isolation rules.
- Added checks for mismatched tenantId scenarios, reinforcing the integrity of tenant context propagation.
- Enhanced test coverage for async context propagation and mutation guards, improving overall robustness of tenant isolation functionality.

* fix: Remove duplicate re-exports in utils/index.ts

Merge artifact caused `string` and `tempChatRetention` to be exported
twice, which produces TypeScript compile errors for duplicate bindings.

* fix: Resolve admin capability gap in multi-tenant mode (TODO #12091)

- hasCapabilityForPrincipals now queries both tenant-scoped AND
  platform-level grants when tenantId is set, so seeded ADMIN grants
  remain effective in tenant mode.
- Add applyTenantIsolation to SystemGrant model factory.

* fix: Harden tenant isolation plugin

- Add replaceGuard for replaceOne/findOneAndReplace to prevent
  cross-tenant document reassignment via replacement documents.
- Cache isStrict() result to avoid process.env reads on every query.
  Export _resetStrictCache() for test teardown.
- Replace console.warn with project logger (winston).
- Add 5 new tests for replace guard behavior (46 total).

* style: Fix import ordering in convo.ts and message.ts

Move type imports after value imports per project style guide.

* fix: Remove tenant isolation from SystemGrant, stamp tenantId in replaceGuard

- SystemGrant is a cross-tenant control plane whose methods handle
  tenantId conditions explicitly. Applying the isolation plugin
  injects a hard equality filter that overrides the $and/$or logic
  in hasCapabilityForPrincipals, making platform-level ADMIN grants
  invisible in tenant mode.
- replaceGuard now stamps tenantId into replacement documents when
  absent, preventing replaceOne from silently stripping tenant
  context. Replacements with a matching tenantId are allowed;
  mismatched tenantId still throws.

* test: Add multi-tenant unique constraint and replace stamping tests

- Verify same name/email can exist in different tenants (compound
  unique index allows it).
- Verify duplicate within same tenant is rejected (E11000).
- Verify tenant-scoped query returns only the correct document.
- Update replaceOne test to assert tenantId is stamped into
  replacement document.
- Add test for replacement with matching tenantId.

* style: Reorder imports in message.ts to align with project style guide

* feat: Add migration to drop superseded unique indexes for multi-tenancy

Existing deployments have single-field unique indexes (e.g. { email: 1 })
that block multi-tenant operation — same email in different tenants
triggers E11000. Mongoose autoIndex creates the new compound indexes
but never drops the old ones.

dropSupersededTenantIndexes() drops all 19 superseded indexes across 11
collections. It is idempotent, skips missing indexes/collections, and
is a no-op on fresh databases.

Must be called before enabling multi-tenant middleware on an existing
deployment. Single-tenant deployments are unaffected (old indexes
coexist harmlessly until migration runs).

Includes 11 tests covering:
- Full upgrade simulation (create old indexes, drop them, verify gone)
- Multi-tenant writes work after migration (same email, different tenant)
- Intra-tenant uniqueness preserved (duplicate within tenant rejected)
- Fresh database (no-op, no errors)
- Partial migration (some collections exist, some don't)
- SUPERSEDED_INDEXES coverage validation

* fix: Update systemGrant test — platform grants now satisfy tenant queries

The TODO #12091 fix intentionally changed hasCapabilityForPrincipals to
match both tenant-scoped AND platform-level grants. The test expected
the old behavior (platform grant invisible to tenant query). Updated
test name and expectation to match the new semantics.

* fix: Align getCapabilitiesForPrincipal with hasCapabilityForPrincipals tenant query

getCapabilitiesForPrincipal used a hard tenantId equality filter while
hasCapabilityForPrincipals uses $and/$or to match both tenant-scoped
and platform-level grants. This caused the two functions to disagree
on what grants a principal holds in tenant mode.

Apply the same $or pattern: when tenantId is provided, match both
{ tenantId } and { tenantId: { $exists: false } }.

Adds test verifying platform-level ADMIN grants appear in
getCapabilitiesForPrincipal when called with a tenantId.

* fix: Remove categories from tenant index migration

categoriesSchema is exported but never used to create a Mongoose model.
No Category model factory exists, no code constructs a model from it,
and no categories collection exists in production databases. Including
it in the migration would attempt to drop indexes from a non-existent
collection (harmlessly skipped) but implies the collection is managed.

* fix: Restrict runAsSystem to async callbacks only

Sync callbacks returning Mongoose thenables silently lose ALS context —
the system bypass does nothing and strict mode throws with no indication
runAsSystem was involved. Narrowing to () => Promise<T> makes the wrong
pattern a compile error. All existing call sites already use async.

* fix: Use next(err) consistently in insertMany pre-hook

The hook accepted a next callback but used throw for errors. Standardize
on next(err) for all error paths so the hook speaks one language —
callback-style throughout.

* fix: Replace optional chaining with explicit null assertions in spendTokens tests

Optional chaining on test assertions masks failures with unintelligible
error messages. Add expect(result).not.toBeNull() before accessing
properties, so a null result produces a clear diagnosis instead of
"received value must be a number".
2026-03-10 23:15:54 -04:00
..
misc/ferretdb 🗑️ chore: Remove Deprecated Project Model and Associated Fields (#11773) 2026-03-10 23:15:53 -04:00
src 🏢 feat: Multi-Tenant Data Isolation Infrastructure (#12091) 2026-03-10 23:15:54 -04:00
.gitignore 📦 refactor: Move DB Models to @librechat/data-schemas (#6210) 2025-03-07 11:55:44 -05:00
babel.config.cjs 📦 refactor: Move DB Models to @librechat/data-schemas (#6210) 2025-03-07 11:55:44 -05:00
jest.config.mjs 🐘 feat: FerretDB Compatibility (#11769) 2026-03-10 23:15:52 -04:00
LICENSE 🔏 fix: Enhance Two-Factor Authentication (#6247) 2025-03-08 15:28:27 -05:00
package.json v0.8.3 (#12161) 2026-03-09 15:19:57 -04:00
README.md 🔐 feat: Granular Role-based Permissions + Entra ID Group Discovery (#7804) 2025-08-13 16:24:17 -04:00
rollup.config.js 📦 refactor: Consolidate DB models, encapsulating Mongoose usage in data-schemas (#11830) 2026-03-10 23:15:53 -04:00
tsconfig.build.json 📦 refactor: Consolidate DB models, encapsulating Mongoose usage in data-schemas (#11830) 2026-03-10 23:15:53 -04:00
tsconfig.json 📦 refactor: Consolidate DB models, encapsulating Mongoose usage in data-schemas (#11830) 2026-03-10 23:15:53 -04:00
tsconfig.spec.json 📦 refactor: Move DB Models to @librechat/data-schemas (#6210) 2025-03-07 11:55:44 -05:00

LibreChat Data Schemas Package

This package provides the database schemas, models, types, and methods for LibreChat using Mongoose ODM.

📁 Package Structure

packages/data-schemas/
├── src/
│   ├── schema/         # Mongoose schema definitions
│   ├── models/         # Model factory functions
│   ├── types/          # TypeScript type definitions
│   ├── methods/        # Database operation methods
│   ├── common/         # Shared constants and enums
│   ├── config/         # Configuration files (winston, etc.)
│   └── index.ts        # Main package exports

🏗️ Architecture Patterns

1. Schema Files (src/schema/)

Schema files define the Mongoose schema structure. They follow these conventions:

  • Naming: Use lowercase filenames (e.g., user.ts, accessRole.ts)
  • Imports: Import types from ~/types for TypeScript support
  • Exports: Export only the schema as default

Example:

import { Schema } from 'mongoose';
import type { IUser } from '~/types';

const userSchema = new Schema<IUser>(
  {
    name: { type: String },
    email: { type: String, required: true },
    // ... other fields
  },
  { timestamps: true }
);

export default userSchema;

2. Type Definitions (src/types/)

Type files define TypeScript interfaces and types. They follow these conventions:

  • Base Type: Define a plain type without Mongoose Document properties
  • Document Interface: Extend the base type with Document and _id
  • Enums/Constants: Place related enums in the type file or common/ if shared

Example:

import type { Document, Types } from 'mongoose';

export type User = {
  name?: string;
  email: string;
  // ... other fields
};

export type IUser = User &
  Document & {
    _id: Types.ObjectId;
  };

3. Model Factory Functions (src/models/)

Model files create Mongoose models using factory functions. They follow these conventions:

  • Function Name: create[EntityName]Model
  • Singleton Pattern: Check if model exists before creating
  • Type Safety: Use the corresponding interface from types

Example:

import userSchema from '~/schema/user';
import type * as t from '~/types';

export function createUserModel(mongoose: typeof import('mongoose')) {
  return mongoose.models.User || mongoose.model<t.IUser>('User', userSchema);
}

4. Database Methods (src/methods/)

Method files contain database operations for each entity. They follow these conventions:

  • Function Name: create[EntityName]Methods
  • Return Type: Export a type for the methods object
  • Operations: Include CRUD operations and entity-specific queries

Example:

import type { Model } from 'mongoose';
import type { IUser } from '~/types';

export function createUserMethods(mongoose: typeof import('mongoose')) {
  async function findUserById(userId: string): Promise<IUser | null> {
    const User = mongoose.models.User as Model<IUser>;
    return await User.findById(userId).lean();
  }

  async function createUser(userData: Partial<IUser>): Promise<IUser> {
    const User = mongoose.models.User as Model<IUser>;
    return await User.create(userData);
  }

  return {
    findUserById,
    createUser,
    // ... other methods
  };
}

export type UserMethods = ReturnType<typeof createUserMethods>;

5. Main Exports (src/index.ts)

The main index file exports:

  • createModels() - Factory function for all models
  • createMethods() - Factory function for all methods
  • Type exports from ~/types
  • Shared utilities and constants

🚀 Adding a New Entity

To add a new entity to the data-schemas package, follow these steps:

Step 1: Create the Type Definition

Create src/types/[entityName].ts:

import type { Document, Types } from 'mongoose';

export type EntityName = {
  /** Field description */
  fieldName: string;
  // ... other fields
};

export type IEntityName = EntityName &
  Document & {
    _id: Types.ObjectId;
  };

Step 2: Update Types Index

Add to src/types/index.ts:

export * from './entityName';

Step 3: Create the Schema

Create src/schema/[entityName].ts:

import { Schema } from 'mongoose';
import type { IEntityName } from '~/types';

const entityNameSchema = new Schema<IEntityName>(
  {
    fieldName: { type: String, required: true },
    // ... other fields
  },
  { timestamps: true }
);

export default entityNameSchema;

Step 4: Create the Model Factory

Create src/models/[entityName].ts:

import entityNameSchema from '~/schema/entityName';
import type * as t from '~/types';

export function createEntityNameModel(mongoose: typeof import('mongoose')) {
  return (
    mongoose.models.EntityName || 
    mongoose.model<t.IEntityName>('EntityName', entityNameSchema)
  );
}

Step 5: Update Models Index

Add to src/models/index.ts:

  1. Import the factory function:
import { createEntityNameModel } from './entityName';
  1. Add to the return object in createModels():
EntityName: createEntityNameModel(mongoose),

Step 6: Create Database Methods

Create src/methods/[entityName].ts:

import type { Model, Types } from 'mongoose';
import type { IEntityName } from '~/types';

export function createEntityNameMethods(mongoose: typeof import('mongoose')) {
  async function findEntityById(id: string | Types.ObjectId): Promise<IEntityName | null> {
    const EntityName = mongoose.models.EntityName as Model<IEntityName>;
    return await EntityName.findById(id).lean();
  }

  // ... other methods

  return {
    findEntityById,
    // ... other methods
  };
}

export type EntityNameMethods = ReturnType<typeof createEntityNameMethods>;

Step 7: Update Methods Index

Add to src/methods/index.ts:

  1. Import the methods:
import { createEntityNameMethods, type EntityNameMethods } from './entityName';
  1. Add to the return object in createMethods():
...createEntityNameMethods(mongoose),
  1. Add to the AllMethods type:
export type AllMethods = UserMethods &
  // ... other methods
  EntityNameMethods;

📝 Best Practices

  1. Consistent Naming: Use lowercase for filenames, PascalCase for types/interfaces
  2. Type Safety: Always use TypeScript types, avoid any
  3. JSDoc Comments: Document complex fields and methods
  4. Indexes: Define database indexes in schema files for query performance
  5. Validation: Use Mongoose schema validation for data integrity
  6. Lean Queries: Use .lean() for read operations when you don't need Mongoose document methods

🔧 Common Patterns

Enums and Constants

Place shared enums in src/common/:

// src/common/permissions.ts
export enum PermissionBits {
  VIEW = 1,
  EDIT = 2,
  DELETE = 4,
  SHARE = 8,
}

Compound Indexes

For complex queries, add compound indexes:

schema.index({ field1: 1, field2: 1 });
schema.index(
  { uniqueField: 1 },
  { 
    unique: true, 
    partialFilterExpression: { uniqueField: { $exists: true } }
  }
);

Virtual Properties

Add computed properties using virtuals:

schema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

🧪 Testing

When adding new entities, ensure:

  • Types compile without errors
  • Models can be created successfully
  • Methods handle edge cases (null checks, validation)
  • Indexes are properly defined for query patterns

📚 Resources