Adding Ethora SDK to Your Node.js Backend
This guide will walk you through integrating the Ethora SDK into your existing Node.js backend application.
Table of Contents
Prerequisites
- Node.js 18+ or higher
- TypeScript 5.0+ (for TypeScript projects)
- An existing Node.js backend application (Express, Fastify, NestJS, etc.)
- Ethora API credentials:
ETHORA_CHAT_API_URL
ETHORA_CHAT_APP_ID
ETHORA_CHAT_APP_SECRET
ETHORA_CHAT_BOT_JID (optional, for chatbot features)
API Documentation (Swagger)
Ethora exposes Swagger UI from every running backend instance at:
- Hosted (Ethora main):
https://api.ethoradev.com/api-docs/
- Enterprise/self-hosted:
https://api.<your-domain>/api-docs/
If you are running a separate staging instance, the same pattern applies (e.g. https://api.asterotoken.com/api-docs/).
Installation
Step 1: Install the Package
npm install @ethora/sdk-backend
yarn add @ethora/sdk-backend
pnpm add @ethora/sdk-backend
Step 2: Install Type Definitions (if using TypeScript)
The package includes TypeScript definitions, so no additional @types package is needed.
Environment Configuration
Step 1: Add Environment Variables
Add the following environment variables to your .env file or your environment configuration:
ETHORA_CHAT_API_URL=https://api.ethoradev.com
ETHORA_CHAT_APP_ID=your_app_id_here
ETHORA_CHAT_APP_SECRET=your_app_secret_here
ETHORA_CHAT_BOT_JID=your_bot_jid@domain.com
Step 2: Load Environment Variables
If you're using a .env file, ensure you have dotenv installed and configured:
npm install dotenv
In your main application file (e.g., app.js, server.js, or index.ts):
import dotenv from 'dotenv';
dotenv.config();
Basic Integration
Step 1: Import the SDK
import { getEthoraSDKService } from '@ethora/sdk-backend';
Step 2: Initialize the Service
You can initialize the service in several ways:
Option A: Singleton Pattern (Recommended)
import { getEthoraSDKService } from '@ethora/sdk-backend';
const chatService = getEthoraSDKService();
export default chatService;
Option B: Direct Initialization
import { getEthoraSDKService } from '@ethora/sdk-backend';
const chatService = getEthoraSDKService();
Option C: Dependency Injection (for frameworks like NestJS)
import { Injectable } from '@nestjs/common';
import { getEthoraSDKService } from '@ethora/sdk-backend';
@Injectable()
export class ChatService {
private readonly ethoraService = getEthoraSDKService();
}
Integration Patterns
Pattern 1: Express.js Integration
import express, { Request, Response } from 'express';
import { getEthoraSDKService } from '@ethora/sdk-backend';
import axios from 'axios';
const router = express.Router();
const chatService = getEthoraSDKService();
router.post(
'/workspaces/:workspaceId/chat',
async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const roomData = req.body;
const response = await chatService.createChatRoom(workspaceId, {
title: roomData.title || `Chat Room ${workspaceId}`,
uuid: workspaceId,
type: roomData.type || 'group',
...roomData,
});
res.json({ success: true, data: response });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to create chat room',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
},
);
router.post('/users/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
const userData = req.body;
const response = await chatService.createUser(userId, userData);
res.json({ success: true, data: response });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to create user',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
router.post(
'/workspaces/:workspaceId/chat/users/:userId',
async (req: Request, res: Response) => {
try {
const { workspaceId, userId } = req.params;
await chatService.grantUserAccessToChatRoom(workspaceId, userId);
res.json({ success: true, message: 'Access granted' });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to grant access',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
},
);
router.delete(
'/workspaces/:workspaceId/chat/users/:userId',
async (req: Request, res: Response) => {
try {
const { workspaceId, userId } = req.params;
await chatService.removeUserAccessFromChatRoom(workspaceId, userId);
res.json({ success: true, message: 'Access removed' });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to remove access',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
},
);
router.get('/users/:userId/chat-token', (req: Request, res: Response) => {
try {
const { userId } = req.params;
const token = chatService.createChatUserJwtToken(userId);
res.json({ token });
} catch (error) {
res.status(500).json({ error: 'Failed to generate token' });
}
});
router.get('/users', async (req: Request, res: Response) => {
try {
const { chatName, xmppUsername } = req.query;
const params: any = {};
if (chatName) params.chatName = String(chatName);
if (xmppUsername) params.xmppUsername = String(xmppUsername);
const response = await chatService.getUsers(
Object.keys(params).length > 0 ? params : undefined,
);
res.json({ success: true, data: response });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to get users',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
router.patch('/users', async (req: Request, res: Response) => {
try {
const { users } = req.body;
if (!Array.isArray(users) || users.length === 0) {
return res.status(400).json({ error: 'users must be a non-empty array' });
}
if (users.length > 100) {
return res
.status(400)
.json({ error: 'Maximum 100 users allowed per request' });
}
const response = await chatService.updateUsers(users);
res.json({ success: true, data: response });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to update users',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
export default router;
Pattern 2: NestJS Integration
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { getEthoraSDKService } from '@ethora/sdk-backend';
import axios from 'axios';
@Injectable()
export class ChatService {
private readonly ethoraService = getEthoraSDKService();
async createChatRoom(workspaceId: string, roomData?: any) {
try {
return await this.ethoraService.createChatRoom(workspaceId, roomData);
} catch (error) {
if (axios.isAxiosError(error)) {
throw new HttpException(
{
message: 'Failed to create chat room',
details: error.response?.data,
},
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
throw error;
}
}
async createUser(userId: string, userData?: any) {
try {
return await this.ethoraService.createUser(userId, userData);
} catch (error) {
if (axios.isAxiosError(error)) {
throw new HttpException(
{
message: 'Failed to create user',
details: error.response?.data,
},
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
throw error;
}
}
generateClientToken(userId: string): string {
return this.ethoraService.createChatUserJwtToken(userId);
}
}
import { Controller, Post, Get, Param, Body } from '@nestjs/common';
import { ChatService } from './chat.service';
@Controller('chat')
export class ChatController {
constructor(private readonly chatService: ChatService) {}
@Post('workspaces/:workspaceId/rooms')
async createChatRoom(
@Param('workspaceId') workspaceId: string,
@Body() roomData: any,
) {
return this.chatService.createChatRoom(workspaceId, roomData);
}
@Post('users/:userId')
async createUser(@Param('userId') userId: string, @Body() userData: any) {
return this.chatService.createUser(userId, userData);
}
@Get('users/:userId/token')
getClientToken(@Param('userId') userId: string) {
return { token: this.chatService.generateClientToken(userId) };
}
}
Pattern 3: Fastify Integration
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { getEthoraSDKService } from '@ethora/sdk-backend';
const chatService = getEthoraSDKService();
export async function chatRoutes(fastify: FastifyInstance) {
fastify.post(
'/workspaces/:workspaceId/chat',
async (request: FastifyRequest, reply: FastifyReply) => {
const { workspaceId } = request.params as { workspaceId: string };
const roomData = request.body as any;
try {
const response = await chatService.createChatRoom(
workspaceId,
roomData,
);
return { success: true, data: response };
} catch (error) {
reply.code(500).send({ error: 'Failed to create chat room' });
}
},
);
fastify.get(
'/users/:userId/chat-token',
async (request: FastifyRequest, reply: FastifyReply) => {
const { userId } = request.params as { userId: string };
const token = chatService.createChatUserJwtToken(userId);
return { token };
},
);
}
Common Use Cases
Use Case 1: Workspace Setup Flow
When creating a new workspace, set up the chat room and initial users:
async function setupWorkspaceChat(
workspaceId: string,
userIds: string[],
adminUserId: string,
) {
const chatService = getEthoraSDKService();
try {
await chatService.createChatRoom(workspaceId, {
title: `Workspace ${workspaceId}`,
uuid: workspaceId,
type: 'group',
});
for (const userId of userIds) {
try {
await chatService.createUser(userId, {
firstName: 'User',
lastName: 'Name',
});
} catch (error) {
console.warn(`User ${userId} might already exist`);
}
}
await chatService.grantUserAccessToChatRoom(workspaceId, userIds);
try {
await chatService.grantChatbotAccessToChatRoom(workspaceId);
} catch (error) {
console.warn('Chatbot access not configured or failed');
}
return { success: true };
} catch (error) {
console.error('Failed to setup workspace chat:', error);
throw error;
}
}
Use Case 2: User Onboarding
When a new user joins your platform:
async function onboardNewUser(
userId: string,
userData: { firstName: string; lastName: string; email: string },
) {
const chatService = getEthoraSDKService();
try {
await chatService.createUser(userId, {
firstName: userData.firstName,
lastName: userData.lastName,
email: userData.email,
displayName: `${userData.firstName} ${userData.lastName}`,
});
const clientToken = chatService.createChatUserJwtToken(userId);
return {
success: true,
chatToken: clientToken,
};
} catch (error) {
console.error('Failed to onboard user:', error);
throw error;
}
}
Use Case 3: Adding User to Existing Workspace
When adding a user to an existing workspace:
async function addUserToWorkspace(workspaceId: string, userId: string) {
const chatService = getEthoraSDKService();
try {
try {
await chatService.createUser(userId);
} catch (error) {
}
await chatService.grantUserAccessToChatRoom(workspaceId, userId);
return { success: true };
} catch (error) {
console.error('Failed to add user to workspace:', error);
throw error;
}
}
Use Case 4: Removing User from Workspace
When removing a user from a workspace:
async function removeUserFromWorkspace(workspaceId: string, userId: string) {
const chatService = getEthoraSDKService();
try {
await chatService.removeUserAccessFromChatRoom(workspaceId, userId);
return { success: true };
} catch (error) {
console.error('Failed to remove user from workspace:', error);
throw error;
}
}
async function removeMultipleUsersFromWorkspace(
workspaceId: string,
userIds: string[],
) {
const chatService = getEthoraSDKService();
try {
await chatService.removeUserAccessFromChatRoom(workspaceId, userIds);
return { success: true };
} catch (error) {
console.error('Failed to remove users from workspace:', error);
throw error;
}
}
Use Case 5: Cleanup on Workspace Deletion
When deleting a workspace:
async function cleanupWorkspaceChat(workspaceId: string, userIds: string[]) {
const chatService = getEthoraSDKService();
try {
await chatService.deleteChatRoom(workspaceId);
if (userIds.length > 0) {
try {
await chatService.deleteUsers(userIds);
} catch (error) {
console.warn('Some users might not exist:', error);
}
}
return { success: true };
} catch (error) {
console.error('Failed to cleanup workspace chat:', error);
throw error;
}
}
Use Case 6: Getting Users
Retrieve users from the chat service:
async function getUsersExample() {
const chatService = getEthoraSDKService();
try {
const allUsers = await chatService.getUsers();
console.log(`Total users: ${allUsers.results?.length || 0}`);
const groupChatUsers = await chatService.getUsers({
chatName: 'appId_workspaceId',
});
const oneOnOneUsers = await chatService.getUsers({
chatName: 'userA-userB',
});
return { allUsers, groupChatUsers, oneOnOneUsers };
} catch (error) {
console.error('Failed to get users:', error);
throw error;
}
}
Use Case 7: Updating Users (Batch)
Update multiple users at once:
async function updateUsersExample() {
const chatService = getEthoraSDKService();
try {
const response = await chatService.updateUsers([
{
xmppUsername: 'appId_user1',
firstName: 'John',
lastName: 'Doe',
username: 'johndoe',
profileImage: 'https://example.com/avatar1.jpg',
},
{
xmppUsername: 'appId_user2',
firstName: 'Jane',
lastName: 'Smith',
username: 'janesmith',
},
]);
response.results?.forEach((result: any) => {
if (result.status === 'updated') {
console.log(`User ${result.xmppUsername} updated successfully`);
} else if (result.status === 'not-found') {
console.warn(`User ${result.xmppUsername} not found`);
} else if (result.status === 'skipped') {
console.log(`User ${result.xmppUsername} update skipped`);
}
});
return response;
} catch (error) {
console.error('Failed to update users:', error);
throw error;
}
}
API Reference
Core Methods
createUser(userId: UUID, userData?: Record<string, unknown>): Promise<ApiResponse>
Creates a user in the chat service using the /v2/users/batch endpoint.
Parameters:
userId (UUID): The unique identifier of the user
userData (optional): Additional user data
firstName (string): User's first name
lastName (string): User's last name (minimum 2 characters)
email (string): User's email address
password (string): User's password
displayName (string): Display name (will be split into firstName/lastName if needed)
Returns: Promise resolving to the API response
Note: The API requires lastName to be at least 2 characters. If not provided or too short, defaults to "User".
createChatRoom(chatId: UUID, roomData?: Record<string, unknown>): Promise<ApiResponse>
Creates a chat room using the /v2/chats endpoint.
Parameters:
chatId (UUID): The unique identifier of the chat/workspace
roomData (optional): Room configuration
title (string): Chat room title
uuid (string): Room UUID (defaults to chatId)
type (string): Room type (defaults to "group")
Returns: Promise resolving to the API response
grantUserAccessToChatRoom(chatId: UUID, userId: UUID | UUID[]): Promise<ApiResponse>
Grants user(s) access to a chat room using the /v2/chats/users-access endpoint.
Parameters:
chatId (UUID): The unique identifier of the chat/workspace
userId (UUID | UUID[]): Single user ID or array of user IDs
Returns: Promise resolving to the API response
Note: User IDs are automatically prefixed with {appId}_ if they don't already have the prefix.
removeUserAccessFromChatRoom(chatId: UUID, userId: UUID | UUID[]): Promise<ApiResponse>
Removes user(s) access from a chat room using the /v2/chats/users-access DELETE endpoint.
Parameters:
chatId (UUID): The unique identifier of the chat/workspace
userId (UUID | UUID[]): Single user ID or array of user IDs to remove
Returns: Promise resolving to the API response
Note: User IDs are automatically prefixed with {appId}_ if they don't already have the prefix.
grantChatbotAccessToChatRoom(chatId: UUID): Promise<ApiResponse>
Grants chatbot access to a chat room.
Parameters:
chatId (UUID): The unique identifier of the chat/workspace
Returns: Promise resolving to the API response
Requires: ETHORA_CHAT_BOT_JID environment variable to be set
getUsers(params?: GetUsersQueryParams): Promise<ApiResponse>
Retrieves users from the chat service using the /v2/chats/users endpoint.
Parameters:
params (GetUsersQueryParams, optional): Query parameters
chatName (string): Filter by chat name
- Group chats:
appId_chatId format
- 1-on-1 chats:
xmppUsernameA-xmppUsernameB format
xmppUsername (string): Filter by specific XMPP username
Query Modes:
- No parameters: Returns all users of the app
- With
chatName: Returns all users of the specified chat
- With
xmppUsername: Returns a specific user
Returns: Promise resolving to the API response with users array
updateUsers(users: UpdateUserData[]): Promise<ApiResponse>
Updates multiple users at once using the /v2/chats/users PATCH endpoint.
Parameters:
users (UpdateUserData[]): Array of user data to update (1-100 users)
xmppUsername (string, required): XMPP username to identify the user
firstName (string, optional): First name
lastName (string, optional): Last name
username (string, optional): Username
profileImage (string, optional): Profile image URL
Returns: Promise resolving to the API response with results array
Response Status Values:
updated: User was successfully updated (includes updated user data)
not-found: User was not found
skipped: User update was skipped
Limits: 1-100 users per request
deleteUsers(userIds: UUID[]): Promise<ApiResponse>
Deletes users from the chat service using the /v1/users/batch endpoint.
Parameters:
userIds (UUID[]): Array of user IDs to delete
Returns: Promise resolving to the API response
Note: Gracefully handles non-existent users (422 status with "not found").
deleteChatRoom(chatId: UUID): Promise<ApiResponse>
Deletes a chat room using the /v1/chats endpoint.
Parameters:
chatId (UUID): The unique identifier of the chat/workspace
Returns: Promise resolving to the API response
Note: Gracefully handles non-existent rooms (422 status with "not found").
Helper Methods
createChatName(chatId: UUID, full?: boolean): string
Generates a chat room JID from a chat ID.
Parameters:
chatId (UUID): The unique identifier of the chat
full (boolean, optional): Whether to include the full JID domain (default: true)
Returns: The JID string
- Full:
{appId}_{chatId}@conference.xmpp.ethoradev.com
- Short:
{appId}_{chatId}
createChatUserJwtToken(userId: UUID): string
Creates a client-side JWT token for user authentication.
Parameters:
userId (UUID): The unique identifier of the user
Returns: The encoded JWT token for client-side authentication
Error Handling
Handling API Errors
The SDK uses Axios for HTTP requests, so errors are AxiosError instances:
import axios from 'axios';
import { getEthoraSDKService } from '@ethora/sdk-backend';
const chatService = getEthoraSDKService();
async function createChatRoomSafely(workspaceId: string) {
try {
return await chatService.createChatRoom(workspaceId);
} catch (error) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const errorData = error.response?.data;
if (status === 422) {
console.error('Validation error:', errorData);
} else if (status === 401) {
console.error('Authentication failed - check your credentials');
} else if (status === 404) {
console.error('Resource not found');
} else {
console.error(`HTTP error ${status}:`, errorData);
}
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
Graceful Error Handling for Idempotent Operations
Some operations are idempotent and can be safely retried:
async function ensureChatRoomExists(workspaceId: string) {
const chatService = getEthoraSDKService();
try {
await chatService.createChatRoom(workspaceId);
} catch (error) {
if (axios.isAxiosError(error)) {
const errorData = error.response?.data;
const errorMessage =
typeof errorData === 'object' && errorData !== null
? (errorData as { error?: string }).error || ''
: String(errorData || '');
if (
error.response?.status === 422 &&
(errorMessage.includes('already exist') ||
errorMessage.includes('already exists'))
) {
console.log('Chat room already exists, continuing...');
return;
}
}
throw error;
}
}
Best Practices
1. Use Singleton Pattern
The SDK provides a singleton instance. Reuse it rather than creating multiple instances:
const chatService = getEthoraSDKService();
const chatService1 = getEthoraSDKService();
const chatService2 = getEthoraSDKService();
2. Centralize Chat Service
Create a service wrapper in your application:
import { getEthoraSDKService } from '@ethora/sdk-backend';
import type { ChatRepository } from '@ethora/sdk-backend';
class ChatServiceWrapper {
private service: ChatRepository;
constructor() {
this.service = getEthoraSDKService();
}
async setupWorkspace(workspaceId: string, userIds: string[]) {
await this.service.createChatRoom(workspaceId);
}
getService() {
return this.service;
}
}
export default new ChatServiceWrapper();
3. Environment Variable Validation
Validate environment variables on application startup:
function validateEthoraConfig() {
const required = [
'ETHORA_CHAT_API_URL',
'ETHORA_CHAT_APP_ID',
'ETHORA_CHAT_APP_SECRET',
];
const missing = required.filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(
`Missing required Ethora environment variables: ${missing.join(', ')}`,
);
}
}
validateEthoraConfig();
4. Logging Integration
Integrate with your existing logging system:
import { getEthoraSDKService } from '@ethora/sdk-backend';
import { logger } from './utils/logger';
const chatService = getEthoraSDKService();
async function createChatRoomWithLogging(workspaceId: string) {
logger.info(`Creating chat room for workspace: ${workspaceId}`);
try {
const result = await chatService.createChatRoom(workspaceId);
logger.info(`Chat room created successfully: ${workspaceId}`);
return result;
} catch (error) {
logger.error(`Failed to create chat room: ${workspaceId}`, error);
throw error;
}
}
5. Type Safety
Use TypeScript types from the SDK:
import type { UUID, ApiResponse } from '@ethora/sdk-backend';
async function createUserTyped(
userId: UUID,
userData: {
firstName: string;
lastName: string;
email: string;
},
): Promise<ApiResponse> {
const chatService = getEthoraSDKService();
return await chatService.createUser(userId, userData);
}
Troubleshooting
Issue: "Missing required environment variables"
Solution: Ensure all required environment variables are set:
ETHORA_CHAT_API_URL=https://api.ethoradev.com
ETHORA_CHAT_APP_ID=your_app_id
ETHORA_CHAT_APP_SECRET=your_app_secret
Issue: "Authentication failed" (401 errors)
Solution: Verify your ETHORA_CHAT_APP_SECRET is correct and matches your app ID.
Issue: "User already exists" errors
Solution: Handle idempotent operations gracefully:
try {
await chatService.createUser(userId);
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 422) {
console.log('User already exists');
} else {
throw error;
}
}
Issue: "Chat room not found" during deletion
Solution: The SDK handles this gracefully. The deleteChatRoom method returns { ok: false, reason: "Chat room not found" } if the room doesn't exist, which is safe to ignore.
Issue: TypeScript compilation errors
Solution: Ensure you're using TypeScript 5.0+ and have proper type definitions:
npm install --save-dev typescript@^5.0.0
Next Steps
Support
For issues, questions, or contributions, please refer to the main README.md file.
TypeScript Configuration
The project uses strict TypeScript settings. See tsconfig.json for details.
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
License
Apache 2.0
Support
For issues and questions, please open an issue on the GitHub repository.
To run tests with logs run from root:
TEST_LOG_FILE=logs/chat-repo.log npm test