Service Layer
Services in LibreChat encapsulate complex business logic, coordinate between models, and provide reusable functionality across the application. They keep controllers thin and promote code reuse.Service Organization
Services are organized inapi/server/services/:
api/server/services/
├── AuthService.js
├── PermissionService.js
├── ToolService.js
├── PluginService.js
├── ActionService.js
├── GraphApiService.js
├── Config/
│ ├── getAppConfig.js
│ └── loadModels.js
├── Files/
│ ├── process.js
│ └── S3/
└── ...
Authentication Service
File:api/server/services/AuthService.js
User Registration
const bcrypt = require('bcryptjs');
const { isEmailDomainAllowed } = require('@librechat/api');
const { createUser, countUsers, findUser } = require('~/models');
const { sendEmail } = require('~/server/utils');
const registerUser = async (user, additionalData = {}) => {
const { email, password, name, username } = user;
try {
const appConfig = await getAppConfig();
// Validate email domain
if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
return {
status: 403,
message: 'Email address not allowed'
};
}
// Check if user exists
const existingUser = await findUser({ email }, 'email _id');
if (existingUser) {
return { status: 200, message: 'User already exists' };
}
// Determine if first user (becomes admin)
const isFirstRegisteredUser = (await countUsers()) === 0;
// Hash password
const salt = bcrypt.genSaltSync(10);
const newUserData = {
provider: 'local',
email,
username,
name,
role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER,
password: bcrypt.hashSync(password, salt),
...additionalData,
};
const newUser = await createUser(newUserData, appConfig.balance);
// Send verification email
if (emailEnabled && !newUser.emailVerified) {
await sendVerificationEmail({ _id: newUser._id, email, name });
}
return { status: 200, message: 'Registration successful' };
} catch (err) {
logger.error('[registerUser]', err);
return { status: 500, message: 'Something went wrong' };
}
};
Token Management
const jwt = require('jsonwebtoken');
const { DEFAULT_SESSION_EXPIRY, DEFAULT_REFRESH_TOKEN_EXPIRY } = require('@librechat/data-schemas');
const { createSession, generateToken, generateRefreshToken } = require('~/models');
const setAuthTokens = async (userId, res, _session = null) => {
try {
let session = _session;
let refreshToken;
let refreshTokenExpires;
if (session && session._id) {
refreshTokenExpires = session.expiration.getTime();
refreshToken = await generateRefreshToken(session);
} else {
const result = await createSession(userId, {
expiresIn: DEFAULT_REFRESH_TOKEN_EXPIRY,
});
session = result.session;
refreshToken = result.refreshToken;
refreshTokenExpires = session.expiration.getTime();
}
const user = await getUserById(userId);
const token = await generateToken(user, DEFAULT_SESSION_EXPIRY);
// Set cookies
res.cookie('refreshToken', refreshToken, {
expires: new Date(refreshTokenExpires),
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});
res.cookie('token_provider', 'librechat', {
expires: new Date(refreshTokenExpires),
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});
return token;
} catch (error) {
logger.error('[setAuthTokens]', error);
throw error;
}
};
Password Reset
const { webcrypto } = require('node:crypto');
const { createToken, findToken, deleteTokens } = require('~/models');
const createTokenHash = () => {
const token = Buffer.from(webcrypto.getRandomValues(new Uint8Array(32))).toString('hex');
const hash = bcrypt.hashSync(token, 10);
return [token, hash];
};
const requestPasswordReset = async (req) => {
const { email } = req.body;
const user = await findUser({ email }, 'email _id');
if (!user) {
// Return generic message for security
return { message: 'If account exists, reset link has been sent.' };
}
await deleteTokens({ userId: user._id });
const [resetToken, hash] = createTokenHash();
await createToken({
userId: user._id,
token: hash,
createdAt: Date.now(),
expiresIn: 900, // 15 minutes
});
const link = `${process.env.DOMAIN_CLIENT}/reset-password?token=${resetToken}&userId=${user._id}`;
await sendEmail({
email: user.email,
subject: 'Password Reset Request',
payload: { name: user.name, link },
template: 'requestPasswordReset.handlebars',
});
return { message: 'Password reset link sent.' };
};
const resetPassword = async (userId, token, password) => {
const passwordResetToken = await findToken({ userId }, { sort: { createdAt: -1 } });
if (!passwordResetToken) {
return new Error('Invalid or expired token');
}
const isValid = bcrypt.compareSync(token, passwordResetToken.token);
if (!isValid) {
return new Error('Invalid or expired token');
}
const hash = bcrypt.hashSync(password, 10);
await updateUser(userId, { password: hash });
await deleteTokens({ token: passwordResetToken.token });
return { message: 'Password reset successful' };
};
Permission Service
File:api/server/services/PermissionService.js
Handles Access Control Lists (ACL) for resources:
Grant Permission
const mongoose = require('mongoose');
const { PrincipalType, ResourceType } = require('librechat-data-provider');
const { AclEntry, AccessRole } = require('~/db/models');
const grantPermission = async ({
principalType,
principalId,
resourceType,
resourceId,
accessRoleId,
grantedBy,
session,
}) => {
try {
// Validate principal type
if (!Object.values(PrincipalType).includes(principalType)) {
throw new Error(`Invalid principal type: ${principalType}`);
}
// Validate resource
if (!resourceId || !mongoose.Types.ObjectId.isValid(resourceId)) {
throw new Error(`Invalid resource ID: ${resourceId}`);
}
// Get role to determine permission bits
const role = await findRoleByIdentifier(accessRoleId);
if (!role) {
throw new Error(`Role ${accessRoleId} not found`);
}
// Ensure role is for correct resource type
if (role.resourceType !== resourceType) {
throw new Error(
`Role ${accessRoleId} is for ${role.resourceType}, not ${resourceType}`,
);
}
return await grantPermissionACL(
principalType,
principalId,
resourceType,
resourceId,
role.permBits,
grantedBy,
session,
role._id,
);
} catch (error) {
logger.error(`[PermissionService.grantPermission] ${error.message}`);
throw error;
}
};
Check Permission
const checkPermission = async ({
userId,
role,
resourceType,
resourceId,
requiredPermission
}) => {
try {
if (typeof requiredPermission !== 'number' || requiredPermission < 1) {
throw new Error('requiredPermission must be a positive number');
}
validateResourceType(resourceType);
// Get all principals for user (user + groups + public)
const principals = await getUserPrincipals({ userId, role });
if (principals.length === 0) {
return false;
}
return await hasPermission(principals, resourceType, resourceId, requiredPermission);
} catch (error) {
logger.error(`[PermissionService.checkPermission] ${error.message}`);
return false;
}
};
Find Accessible Resources
const findAccessibleResources = async ({
userId,
role,
resourceType,
requiredPermissions
}) => {
try {
if (typeof requiredPermissions !== 'number' || requiredPermissions < 1) {
throw new Error('requiredPermissions must be a positive number');
}
validateResourceType(resourceType);
// Get all principals for the user
const principalsList = await getUserPrincipals({ userId, role });
if (principalsList.length === 0) {
return [];
}
return await findAccessibleResourcesACL(
principalsList,
resourceType,
requiredPermissions
);
} catch (error) {
logger.error(`[PermissionService.findAccessibleResources] ${error.message}`);
return [];
}
};
Tool Service
File:api/server/services/ToolService.js
Handles tool loading and management for AI agents:
Load Tool Definitions
const { loadToolDefinitions } = require('@librechat/api');
const { AgentCapabilities, Tools } = require('librechat-data-provider');
async function loadToolDefinitionsWrapper({ req, res, agent, tool_resources }) {
if (!agent.tools || agent.tools.length === 0) {
return { toolDefinitions: [] };
}
const appConfig = req.config;
const endpointsConfig = await getEndpointsConfig(req);
const enabledCapabilities = new Set(
endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []
);
const checkCapability = (capability) => enabledCapabilities.has(capability);
// Filter tools based on enabled capabilities
const filteredTools = agent.tools?.filter((tool) => {
if (tool === Tools.file_search) {
return checkCapability(AgentCapabilities.file_search);
}
if (tool === Tools.execute_code) {
return checkCapability(AgentCapabilities.execute_code);
}
if (tool === Tools.web_search) {
return checkCapability(AgentCapabilities.web_search);
}
return true;
});
if (!filteredTools || filteredTools.length === 0) {
return { toolDefinitions: [] };
}
const { toolDefinitions, toolRegistry } = await loadToolDefinitions(
{
userId: req.user.id,
agentId: agent.id,
tools: filteredTools,
},
{
isBuiltInTool,
loadAuthValues,
getOrFetchMCPServerTools,
},
);
return { toolDefinitions, toolRegistry };
}
Load Tools for Execution
const { loadTools } = require('~/app/clients/tools/util');
async function loadToolsForExecution({
req,
res,
agent,
toolNames,
toolRegistry,
userMCPAuthMap,
}) {
const appConfig = req.config;
const allLoadedTools = [];
const configurable = { userMCPAuthMap };
// Filter out special tools
const regularToolNames = toolNames.filter((name) =>
!name.includes('__TOOL_SEARCH__') && !name.includes('__PTC__')
);
if (regularToolNames.length > 0) {
const { loadedTools } = await loadTools({
agent,
userMCPAuthMap,
tools: regularToolNames,
user: req.user.id,
options: {
req,
res,
processFileURL,
uploadImageBuffer,
},
webSearch: appConfig?.webSearch,
fileStrategy: appConfig?.fileStrategy,
});
if (loadedTools) {
allLoadedTools.push(...loadedTools);
}
}
return { configurable, loadedTools: allLoadedTools };
}
Service Best Practices
1. Single Responsibility
// Good: Focused service function
const sendVerificationEmail = async (user) => {
const token = generateToken();
await saveToken(user._id, token);
await sendEmail({ to: user.email, token });
};
// Avoid: Doing too much
const registerAndSendEmail = async (userData) => {
// Registration logic
// Email logic
// Token logic
// All mixed together
};
2. Error Handling
const serviceFunction = async (params) => {
try {
const result = await operation(params);
return result;
} catch (error) {
logger.error('[serviceFunction]', error);
throw new Error('Service operation failed');
}
};
3. Return Consistent Types
// Good: Always returns same type
const getUser = async (id) => {
const user = await findUser(id);
if (!user) {
return null; // Or throw error
}
return user;
};
// Avoid: Mixed return types
const getUser = async (id) => {
const user = await findUser(id);
if (!user) {
return { error: 'Not found' }; // Inconsistent
}
return user;
};
4. Use Dependency Injection
// Good: Dependencies passed in
const createService = ({ emailService, tokenService }) => ({
register: async (user) => {
const token = await tokenService.generate();
await emailService.send(user.email, token);
},
});
// Avoid: Hard-coded dependencies
const register = async (user) => {
const token = generateToken(); // Hard-coded
await sendEmail(user.email); // Hard-coded
};
5. Validate Input
const processData = async (data) => {
if (!data || typeof data !== 'object') {
throw new Error('Invalid input data');
}
if (!data.userId || !data.resourceId) {
throw new Error('Missing required fields');
}
// Process valid data
};
Testing Services
const { registerUser } = require('~/server/services/AuthService');
describe('AuthService', () => {
describe('registerUser', () => {
it('should create a new user', async () => {
const userData = {
email: '[email protected]',
password: 'password123',
name: 'Test User',
};
const result = await registerUser(userData);
expect(result.status).toBe(200);
expect(result.message).toBe('Registration successful');
});
it('should reject duplicate email', async () => {
const result = await registerUser({ email: '[email protected]' });
expect(result.status).toBe(200); // Generic response for security
});
});
});
Related Documentation
- Controllers - Controller patterns
- Models - Database models
- Routes - Route structure