Documentation Index
Fetch the complete documentation index at: https://mintlify.com/danny-avila/librechat/llms.txt
Use this file to discover all available pages before exploring further.
Database Models (Mongoose)
LibreChat uses Mongoose for MongoDB modeling. Models define the data structure, validation, and business logic for database operations.Model Organization
Models are organized inapi/models/ and api/db/models.js:
api/models/
├── index.js # Model exports
├── Conversation.js # Conversation operations
├── Message.js # Message operations
├── Prompt.js # Prompt operations
├── User.js # User methods
├── Preset.js # Preset operations
└── ...
api/db/
└── models.js # Mongoose model definitions
Model Definition
Models are created using@librechat/data-schemas:
File: api/db/models.js
const mongoose = require('mongoose');
const { createModels } = require('@librechat/data-schemas');
const models = createModels(mongoose);
module.exports = { ...models };
User- User accountsConversation- Chat conversationsMessage- Chat messagesPrompt- Prompt templatesPromptGroup- Prompt collectionsAssistant- AI assistantsAgent- AI agentsFile- File metadataTransaction- Token transactionsBalance- User balancesRole- User rolesAclEntry- Access control
Model Operations
Conversation Model
File:api/models/Conversation.js
Get Single Conversation
const { Conversation } = require('~/db/models');
const getConvo = async (user, conversationId) => {
try {
return await Conversation.findOne({ user, conversationId }).lean();
} catch (error) {
logger.error('[getConvo] Error getting conversation', error);
throw new Error('Error getting conversation');
}
};
Save Conversation
const { getMessages } = require('./Message');
const saveConvo = async (req, { conversationId, newConversationId, ...convo }, metadata) => {
try {
const messages = await getMessages({ conversationId }, '_id');
const update = { ...convo, messages, user: req.user.id };
if (newConversationId) {
update.conversationId = newConversationId;
}
const conversation = await Conversation.findOneAndUpdate(
{ conversationId, user: req.user.id },
{ $set: update },
{ new: true, upsert: true },
);
return conversation.toObject();
} catch (error) {
logger.error('[saveConvo] Error saving conversation', error);
return { message: 'Error saving conversation' };
}
};
Get Conversations with Cursor Pagination
const getConvosByCursor = async (
user,
{ cursor, limit = 25, isArchived = false, tags, search } = {},
) => {
const filters = [{ user }];
// Archive filter
if (isArchived) {
filters.push({ isArchived: true });
} else {
filters.push({ $or: [{ isArchived: false }, { isArchived: { $exists: false } }] });
}
// Tag filter
if (Array.isArray(tags) && tags.length > 0) {
filters.push({ tags: { $in: tags } });
}
// Search filter
if (search) {
const meiliResults = await Conversation.meiliSearch(search, {
filter: `user = "${user}"`,
});
const matchingIds = meiliResults.hits.map((result) => result.conversationId);
filters.push({ conversationId: { $in: matchingIds } });
}
// Cursor pagination
let cursorFilter = null;
if (cursor) {
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString());
cursorFilter = {
$or: [
{ updatedAt: { $lt: new Date(decoded.updatedAt) } },
{ updatedAt: new Date(decoded.updatedAt), _id: { $lt: decoded._id } },
],
};
filters.push(cursorFilter);
}
const query = { $and: filters };
const convos = await Conversation.find(query)
.select('conversationId endpoint title createdAt updatedAt')
.sort({ updatedAt: -1 })
.limit(limit + 1)
.lean();
let nextCursor = null;
if (convos.length > limit) {
const lastItem = convos[limit - 1];
const composite = {
updatedAt: lastItem.updatedAt.toISOString(),
_id: lastItem._id.toString(),
};
nextCursor = Buffer.from(JSON.stringify(composite)).toString('base64');
convos.pop();
}
return { conversations: convos, nextCursor };
};
Delete Conversations
const { deleteMessages } = require('./Message');
const deleteConvos = async (user, filter) => {
try {
const userFilter = { ...filter, user };
const conversations = await Conversation.find(userFilter).select('conversationId');
const conversationIds = conversations.map((c) => c.conversationId);
if (!conversationIds.length) {
throw new Error('Conversation not found or already deleted.');
}
const deleteConvoResult = await Conversation.deleteMany(userFilter);
const deleteMessagesResult = await deleteMessages({
conversationId: { $in: conversationIds },
});
return { ...deleteConvoResult, messages: deleteMessagesResult };
} catch (error) {
logger.error('[deleteConvos] Error deleting conversations', error);
throw error;
}
};
Message Model
File:api/models/Message.js
Save Message
const { z } = require('zod');
const { Message } = require('~/db/models');
const idSchema = z.string().uuid();
async function saveMessage(req, params, metadata) {
if (!req?.user?.id) {
throw new Error('User not authenticated');
}
const validConvoId = idSchema.safeParse(params.conversationId);
if (!validConvoId.success) {
logger.warn(`Invalid conversation ID: ${params.conversationId}`);
return;
}
try {
const update = {
...params,
user: req.user.id,
messageId: params.newMessageId || params.messageId,
};
const message = await Message.findOneAndUpdate(
{ messageId: params.messageId, user: req.user.id },
update,
{ upsert: true, new: true },
);
return message.toObject();
} catch (err) {
logger.error('Error saving message:', err);
throw err;
}
}
Get Messages
async function getMessages(filter, select) {
try {
if (select) {
return await Message.find(filter)
.select(select)
.sort({ createdAt: 1 })
.lean();
}
return await Message.find(filter).sort({ createdAt: 1 }).lean();
} catch (err) {
logger.error('Error getting messages:', err);
throw err;
}
}
Bulk Save Messages
async function bulkSaveMessages(messages, overrideTimestamp = false) {
try {
const bulkOps = messages.map((message) => ({
updateOne: {
filter: { messageId: message.messageId },
update: message,
timestamps: !overrideTimestamp,
upsert: true,
},
}));
const result = await Message.bulkWrite(bulkOps);
return result;
} catch (err) {
logger.error('Error saving messages in bulk:', err);
throw err;
}
}
Prompt Model
File:api/models/Prompt.js
Create Prompt Group
const { PromptGroup, Prompt } = require('~/db/models');
const createPromptGroup = async (saveData) => {
try {
const { prompt, group, author, authorName } = saveData;
// Create or get prompt group
let newPromptGroup = await PromptGroup.findOneAndUpdate(
{ ...group, author, authorName, productionId: null },
{ $setOnInsert: { ...group, author, authorName, productionId: null } },
{ new: true, upsert: true },
)
.lean()
.select('-__v')
.exec();
// Create prompt
const newPrompt = await Prompt.findOneAndUpdate(
{ ...prompt, author, groupId: newPromptGroup._id },
{ $setOnInsert: { ...prompt, author, groupId: newPromptGroup._id } },
{ new: true, upsert: true },
)
.lean()
.select('-__v')
.exec();
// Set as production prompt
newPromptGroup = await PromptGroup.findByIdAndUpdate(
newPromptGroup._id,
{ productionId: newPrompt._id },
{ new: true },
)
.lean()
.select('-__v')
.exec();
return {
prompt: newPrompt,
group: {
...newPromptGroup,
productionPrompt: { prompt: newPrompt.prompt },
},
};
} catch (error) {
logger.error('Error saving prompt group', error);
throw new Error('Error saving prompt group');
}
};
Get Prompt Groups with ACL
const { ObjectId } = require('mongodb');
async function getListPromptGroupsByAccess({
accessibleIds = [],
otherParams = {},
limit = null,
after = null,
}) {
const isPaginated = limit !== null;
const normalizedLimit = isPaginated ? Math.min(Math.max(1, parseInt(limit) || 20), 100) : null;
// Build query with ACL filter
const baseQuery = { ...otherParams, _id: { $in: accessibleIds } };
// Add cursor condition
if (after) {
const cursor = JSON.parse(Buffer.from(after, 'base64').toString('utf8'));
const cursorCondition = {
$or: [
{ updatedAt: { $lt: new Date(cursor.updatedAt) } },
{ updatedAt: new Date(cursor.updatedAt), _id: { $gt: new ObjectId(cursor._id) } },
],
};
Object.assign(baseQuery, cursorCondition);
}
// Build aggregation pipeline
const pipeline = [
{ $match: baseQuery },
{ $sort: { updatedAt: -1, _id: 1 } },
];
if (isPaginated) {
pipeline.push({ $limit: normalizedLimit + 1 });
}
// Lookup production prompt
pipeline.push(
{
$lookup: {
from: 'prompts',
localField: 'productionId',
foreignField: '_id',
as: 'productionPrompt',
},
},
{ $unwind: { path: '$productionPrompt', preserveNullAndEmptyArrays: true } },
{
$project: {
name: 1,
oneliner: 1,
category: 1,
author: 1,
createdAt: 1,
updatedAt: 1,
'productionPrompt.prompt': 1,
},
},
);
const promptGroups = await PromptGroup.aggregate(pipeline).exec();
const hasMore = isPaginated ? promptGroups.length > normalizedLimit : false;
const data = isPaginated ? promptGroups.slice(0, normalizedLimit) : promptGroups;
let nextCursor = null;
if (isPaginated && hasMore && data.length > 0) {
const lastGroup = promptGroups[normalizedLimit - 1];
nextCursor = Buffer.from(
JSON.stringify({
updatedAt: lastGroup.updatedAt.toISOString(),
_id: lastGroup._id.toString(),
}),
).toString('base64');
}
return {
object: 'list',
data,
has_more: hasMore,
after: nextCursor,
};
}
Model Best Practices
1. Always Use Try-Catch
const getItem = async (id) => {
try {
return await Model.findById(id).lean();
} catch (error) {
logger.error('[getItem]', error);
throw new Error('Error fetching item');
}
};
2. Use Lean for Read-Only Operations
// Good: Use lean() for read-only queries
const items = await Model.find(filter).lean();
// Only skip lean() when you need document methods
const doc = await Model.findById(id);
await doc.save();
3. Select Only Required Fields
// Select specific fields
const user = await User.findById(id).select('name email').lean();
// Exclude sensitive fields
const user = await User.findById(id).select('-password -totpSecret').lean();
4. Use Indexes for Queries
// Compound index for common queries
conversationSchema.index({ user: 1, updatedAt: -1 });
messageSchema.index({ conversationId: 1, createdAt: 1 });
5. Validate Input
const { z } = require('zod');
const idSchema = z.string().uuid();
const saveItem = async (data) => {
const valid = idSchema.safeParse(data.id);
if (!valid.success) {
throw new Error('Invalid ID format');
}
// Continue with save...
};
6. Handle Duplicate Key Errors
try {
await Model.create(data);
} catch (err) {
if (err.code === 11000) {
// Duplicate key error
logger.warn('Duplicate entry detected');
// Handle gracefully
} else {
throw err;
}
}
Related Documentation
- Controllers - Controller patterns
- Services - Service layer
- Routes - Route structure