This guide will walk you through integrating the Ethora SDK into your existing Node.js backend application.
Part of the Ethora SDK ecosystem — see all SDKs, tools, and sample apps. Follow cross-SDK updates in the Release Notes.
- Prerequisites
- Installation
- Environment Configuration
- Basic Integration
- Integration Patterns
- Common Use Cases
- Error Handling
- Best Practices
- Troubleshooting
- 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_URLETHORA_CHAT_APP_IDETHORA_CHAT_APP_SECRET
Ethora exposes Swagger UI from every running backend instance at:
- Hosted (Ethora main):
https://api.chat.ethora.com/api-docs/#/— canonical, regenerated automatically from production. - 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.chat-qa.ethora.com/api-docs/ for Ethora QA).
The SDK also exposes the explicit tenant-admin surface added in the backend:
listApps(),getApp(appId),createApp(appData),deleteApp(appId)createUsersInApp(appId, payload),getUsersBatchJob(appId, jobId),deleteUsersInApp(appId, userIds)createChatRoomInApp(appId, chatId, roomData),deleteChatRoomInApp(appId, chatId)grantUserAccessToChatRoomInApp(appId, chatId, userIds),removeUserAccessFromChatRoomInApp(appId, chatId, userIds)getUserChatsInApp(appId, userId, params),updateChatRoomInApp(appId, chatId, updateData)
These helpers target explicit /v2/apps/{appId}/... routes so a parent-app / tenant backend can manage child apps without relying on implicit token scope.
The Ethora API uses several JWT/token types with different purposes:
B2B Server JWT: used by this SDK automatically for server-to-server API calls. The backend accepts it primarily viax-custom-token, and many deployments also accept it inAuthorization: Bearer ....Client JWT: created bycreateChatUserJwtToken(userId)for client/chat credential flows only.User JWT: returned by Ethora login endpoints and used for user-session API calls outside this SDK's main server-to-server flow.App JWT: legacy app-scoped runtime token. Frontend/bootstrap login and sign-up flows now prefer explicitappId, while old app-token auth remains accepted for backward compatibility.
If you are using explicit tenant-admin routes like /v2/apps/{appId}/..., the intended token for backend integrations is B2B Server JWT.
For new integrations, prefer /v2/... endpoints.
- Main runtime chat/user operations in this SDK use
/v2/.... - Explicit tenant-admin helpers use
/v2/apps/{appId}/.... - A few delete operations still map to legacy
/v1/...endpoints because that is where backend parity currently exists. - Frontend/bootstrap auth flows are moving toward explicit
appIdrequest context rather than relying on implicit app-token scope.
npm install @ethora/sdk-backend
# or
yarn add @ethora/sdk-backend
# or
pnpm add @ethora/sdk-backendThe package includes TypeScript definitions, so no additional @types package is needed.
Add the following environment variables to your .env file or your environment configuration:
# Required
ETHORA_CHAT_API_URL=https://api.chat.ethora.com
ETHORA_CHAT_APP_ID=your_app_id_here
ETHORA_CHAT_APP_SECRET=your_app_secret_here
If you're using a .env file, ensure you have dotenv installed and configured:
npm install dotenvIn your main application file (e.g., app.js, server.js, or index.ts):
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();import { getEthoraSDKService } from '@ethora/sdk-backend';You can initialize the service in several ways:
// services/chatService.ts
import { getEthoraSDKService } from '@ethora/sdk-backend';
// Get the singleton instance
const chatService = getEthoraSDKService();
export default chatService;// In your route handler or service
import { getEthoraSDKService } from '@ethora/sdk-backend';
const chatService = getEthoraSDKService();// chat.service.ts
import { Injectable } from '@nestjs/common';
import { getEthoraSDKService } from '@ethora/sdk-backend';
@Injectable()
export class ChatService {
private readonly ethoraService = getEthoraSDKService();
// Your methods here
}// routes/chat.ts
import express, { Request, Response } from 'express';
import { getEthoraSDKService } from '@ethora/sdk-backend';
import type {
ChatRepository,
ApiResponse,
CreateChatRoomRequest,
CreateUserData,
UpdateUserData,
GetUsersQueryParams,
GetUserChatsQueryParams,
UUID
} from '@ethora/sdk-backend';
import axios from 'axios';
const router = express.Router();
const chatService: ChatRepository = getEthoraSDKService();
// Create a chat room for a workspace
router.post(
'/workspaces/:workspaceId/chat',
async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const roomData: Partial<CreateChatRoomRequest> = req.body;
const response: ApiResponse = 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' });
}
}
},
);
// Create a user
router.post('/users/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
const userData: CreateUserData = req.body;
const response: ApiResponse = 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' });
}
}
});
// Grant user access to chat room
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' });
}
}
},
);
// Remove user access from chat room
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' });
}
}
},
);
// Generate client JWT token
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' });
}
});
// Get users
router.get('/users', async (req: Request, res: Response) => {
try {
const { chatName, xmppUsername } = req.query;
const params: GetUsersQueryParams = {};
if (chatName) params.chatName = String(chatName);
if (xmppUsername) params.xmppUsername = String(xmppUsername);
const response: ApiResponse = 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' });
}
}
});
// Update users (batch)
router.patch('/users', async (req: Request, res: Response) => {
try {
const { users } = req.body as { users: UpdateUserData[] };
if (!Array.isArray(users) || users.length === 0) {
return res.status(400).json({ error: 'users must be a non-empty array' });
}
const response: ApiResponse = 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' });
}
}
});
// Update chat room
router.patch(
'/workspaces/:workspaceId/chat',
async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const updateData: { title?: string; description?: string } = req.body;
const response: ApiResponse = await chatService.updateChatRoom(workspaceId, updateData);
res.json({ success: true, data: response });
} catch (error) {
res.status(500).json({ error: 'Failed to update chat room' });
}
},
);
// Get user chats
router.get('/users/:userId/chats', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
const query: GetUserChatsQueryParams = req.query as unknown as GetUserChatsQueryParams;
const response: ApiResponse = await chatService.getUserChats(userId, query);
res.json({ success: true, data: response });
} catch (error) {
res.status(500).json({ error: 'Failed to get user chats' });
}
});
export default router;// chat/chat.service.ts
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { getEthoraSDKService } from '@ethora/sdk-backend';
import axios from 'axios';
@Injectable()
export class ChatService {
private readonly ethoraService: ChatRepository = getEthoraSDKService();
async createChatRoom(workspaceId: string, roomData?: Partial<CreateChatRoomRequest>): Promise<ApiResponse> {
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?: CreateUserData): Promise<ApiResponse> {
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);
}
}
// chat/chat.controller.ts
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: Partial<CreateChatRoomRequest>,
): Promise<ApiResponse> {
return this.chatService.createChatRoom(workspaceId, roomData);
}
@Post('users/:userId')
async createUser(
@Param('userId') userId: string,
@Body() userData: CreateUserData
): Promise<ApiResponse> {
return this.chatService.createUser(userId, userData);
}
@Get('users/:userId/token')
getClientToken(@Param('userId') userId: string) {
return { token: this.chatService.generateClientToken(userId) };
}
}// routes/chat.ts
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { getEthoraSDKService } from '@ethora/sdk-backend';
const chatService = getEthoraSDKService();
export async function chatRoutes(fastify: FastifyInstance) {
// Create chat room
fastify.post(
'/workspaces/:workspaceId/chat',
async (request: FastifyRequest, reply: FastifyReply): Promise<ApiResponse | void> => {
const { workspaceId } = request.params as { workspaceId: string };
const roomData = request.body as Partial<CreateChatRoomRequest>;
try {
const response: ApiResponse = await chatService.createChatRoom(
workspaceId,
roomData,
);
return { success: true, data: response };
} catch (error) {
reply.code(500).send({ error: 'Failed to create chat room' });
}
},
);
// Generate client token
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 };
},
);
}Create and manage a child app through the explicit B2B admin surface:
const sdk = getEthoraSDKService();
const app = await sdk.createApp({ displayName: 'Tenant Managed Demo' });
const childAppId = String((app as any).app?._id || (app as any).result?._id || '');
await sdk.createUsersInApp(childAppId, {
bypassEmailConfirmation: true,
usersList: [
{ uuid: 'workspace-u1', email: 'workspace-u1@example.com', firstName: 'Workspace', lastName: 'One' },
{ uuid: 'workspace-u2', email: 'workspace-u2@example.com', firstName: 'Workspace', lastName: 'Two' },
],
});
await sdk.createChatRoomInApp(childAppId, 'workspace-room', {
title: 'Workspace Room',
uuid: 'workspace-room',
type: 'public',
});
await sdk.grantUserAccessToChatRoomInApp(childAppId, 'workspace-room', ['workspace-u1', 'workspace-u2']);When creating a new workspace, set up the chat room and initial users:
async function setupWorkspaceChat(
workspaceId: string,
userIds: string[],
adminUserId: string,
): Promise<{ success: boolean }> {
const chatService: ChatRepository = getEthoraSDKService();
try {
// 1. Create chat room
await chatService.createChatRoom(workspaceId, {
title: `Workspace ${workspaceId}`,
uuid: workspaceId,
type: 'group',
});
// 2. Create users (if they don't exist)
for (const userId of userIds) {
try {
await chatService.createUser(userId, {
firstName: 'User',
lastName: 'Name',
});
} catch (error) {
// User might already exist, continue
console.warn(`User ${userId} might already exist`);
}
}
// 3. Grant access to all users
await chatService.grantUserAccessToChatRoom(workspaceId, userIds);
return { success: true };
} catch (error) {
console.error('Failed to setup workspace chat:', error);
throw error;
}
}When a new user joins your platform:
async function onboardNewUser(
userId: string,
userData: { firstName: string; lastName: string; email: string },
): Promise<{ success: boolean; chatToken: string }> {
const chatService: ChatRepository = getEthoraSDKService();
try {
// Create user in chat service
await chatService.createUser(userId, {
firstName: userData.firstName,
lastName: userData.lastName,
email: userData.email,
displayName: `${userData.firstName} ${userData.lastName}`,
});
// Generate client token for frontend
const clientToken: string = chatService.createChatUserJwtToken(userId);
return {
success: true,
chatToken: clientToken,
};
} catch (error) {
console.error('Failed to onboard user:', error);
throw error;
}
}When adding a user to an existing workspace:
async function addUserToWorkspace(
workspaceId: string,
userId: string,
): Promise<{ success: boolean }> {
const chatService: ChatRepository = getEthoraSDKService();
try {
// Ensure user exists
try {
await chatService.createUser(userId);
} catch (error) {
// User might already exist, continue
}
// Grant access to workspace chat room
await chatService.grantUserAccessToChatRoom(workspaceId, userId);
return { success: true };
} catch (error) {
console.error('Failed to add user to workspace:', error);
throw error;
}
}When removing a user from a workspace:
async function removeUserFromWorkspace(
workspaceId: string,
userId: string,
): Promise<{ success: boolean }> {
const chatService: ChatRepository = getEthoraSDKService();
try {
// Remove access from workspace chat room
await chatService.removeUserAccessFromChatRoom(workspaceId, userId);
return { success: true };
} catch (error) {
console.error('Failed to remove user from workspace:', error);
throw error;
}
}
// Remove multiple users at once
async function removeMultipleUsersFromWorkspace(
workspaceId: string,
userIds: string[],
): Promise<{ success: boolean }> {
const chatService: ChatRepository = getEthoraSDKService();
try {
await chatService.removeUserAccessFromChatRoom(workspaceId, userIds);
return { success: true };
} catch (error) {
console.error('Failed to remove users from workspace:', error);
throw error;
}
}When deleting a workspace:
async function cleanupWorkspaceChat(
workspaceId: string,
userIds: string[],
): Promise<{ success: boolean }> {
const chatService: ChatRepository = getEthoraSDKService();
try {
// Delete chat room (handles non-existent gracefully)
await chatService.deleteChatRoom(workspaceId);
// Optionally delete users (if they're no longer needed)
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;
}
}Retrieve users from the chat service:
async function getUsersExample(): Promise<{
allUsers: ApiResponse;
groupChatUsers: ApiResponse;
oneOnOneUsers: ApiResponse
}> {
const chatService: ChatRepository = getEthoraSDKService();
try {
// Get all users
const allUsers: ApiResponse = await chatService.getUsers();
console.log(`Total users: ${allUsers.results?.length || 0}`);
// Get users by chat name (group chat)
const groupChatUsers: ApiResponse = await chatService.getUsers({
chatName: 'appId_workspaceId',
});
// Get users by chat name (1-on-1 chat)
const oneOnOneUsers: ApiResponse = await chatService.getUsers({
chatName: 'userA-userB',
});
return { allUsers, groupChatUsers, oneOnOneUsers };
} catch (error) {
console.error('Failed to get users:', error);
throw error;
}
}Update multiple users at once:
async function updateUsersExample(): Promise<ApiResponse> {
const chatService: ChatRepository = getEthoraSDKService();
try {
// Update multiple users (1-100 users per request)
const response: ApiResponse = await chatService.updateUsers([
{
xmppUsername: 'appId_user1',
firstName: 'John',
lastName: 'Doe',
}
]);
return response;
} catch (error) {
console.error('Failed to update users:', error);
throw error;
}
}Update room title or description:
async function updateRoomExample(): Promise<ApiResponse> {
const chatService: ChatRepository = getEthoraSDKService();
try {
const response: ApiResponse = await chatService.updateChatRoom('workspaceId', {
title: 'New Room Title',
description: 'New Description',
});
return response;
} catch (error) {
console.error('Failed to update room:', error);
throw error;
}
}Retrieve all rooms the user has access to:
async function getUserChatsExample(): Promise<ApiResponse> {
const chatService: ChatRepository = getEthoraSDKService();
try {
const query: GetUserChatsQueryParams = {
limit: 20,
includeMembers: true
};
const response: ApiResponse = await chatService.getUserChats('userId', query);
return response;
} catch (error) {
console.error('Failed to get user chats:', error);
throw error;
}
}Creates a child app through POST /v2/apps.
Starts an async user-batch job through POST /v2/apps/{appId}/users/batch.
createChatRoomInApp(appId: UUID, chatId: UUID, roomData?: Record<string, unknown>): Promise<ApiResponse>
Creates a chat in a target app through POST /v2/apps/{appId}/chats.
grantUserAccessToChatRoomInApp(appId: UUID, chatId: UUID, userId: UUID | UUID[]): Promise<ApiResponse>
Adds user access in a target app through POST /v2/apps/{appId}/chats/users-access.
Deletes a chat in a target app through DELETE /v2/apps/{appId}/chats.
Creates a user in the chat service using the /v2/users/batch endpoint.
Interface: CreateUserData
interface CreateUserData {
email: string; // string: User's email address
firstName: string; // string: User's first name
lastName: string; // string: User's last name (min 2 chars)
password?: string; // string (optional): User's password
displayName?: string; // string (optional): Full display name
}Example Request:
await sdk.createUser("user-uuid-123", {
email: "john@example.com",
firstName: "John",
lastName: "Doe"
});Note: The API requires lastName to be at least 2 characters. If not provided or too short, defaults to "User".
Creates a chat room using the /v2/chats endpoint.
Interface: CreateChatRoomRequest
interface CreateChatRoomRequest {
title: string; // string: The display name of the chat room
uuid: string; // string: The workspace/chat identifier
type: string; // string: The room type (e.g., "group")
}Example Request:
const roomData: CreateChatRoomRequest = {
title: "Engineering",
uuid: "room-abc-123",
type: "group"
};
await sdk.createChatRoom("room-abc-123", roomData);Grants user(s) access to a chat room using the /v2/chats/users-access endpoint.
Example Request:
// Single user
await sdk.grantUserAccessToChatRoom("workspace-123", "user-uuid-456");
// Multiple users
await sdk.grantUserAccessToChatRoom("workspace-123", ["user-1", "user-2"]);Note: User IDs are automatically prefixed with {appId}_ if they don't already have the prefix.
Removes user(s) access from a chat room using the /v2/chats/users-access DELETE endpoint.
Example Request:
await sdk.removeUserAccessFromChatRoom("workspace-123", "user-456");Note: User IDs are automatically prefixed with {appId}_ if they don't already have the prefix.
Retrieves users from the chat service using the /v2/chats/users endpoint.
Parameters:
params(GetUsersQueryParams, optional): Query parameterschatName(string): Filter by chat name- Group chats:
appId_chatIdformat - 1-on-1 chats:
xmppUsernameA-xmppUsernameBformat
- Group chats:
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
Updates multiple users at once using the /v2/chats/users PATCH endpoint.
Interface: UpdateUserData
interface UpdateUserData {
xmppUsername: string; // string: Required (format: {appId}_{userId})
firstName?: string; // string (optional): New first name
lastName?: string; // string (optional): New last name
username?: string; // string (optional): New username
profileImage?: string; // string (optional): URL to profile image
description?: string; // string (optional): User bio/description
email?: string; // string (optional): New email address
}Example Request:
await sdk.updateUsers([
{ xmppUsername: "appId_user1", firstName: "NewName" }
]);Retrieves all rooms the user has access to.
Interface: GetUserChatsQueryParams
interface GetUserChatsQueryParams {
limit?: number; // number (optional): Pagination limit
offset?: number; // number (optional): Pagination offset
includeMembers?: boolean; // boolean (optional): Whether to return member lists
}Example Request:
const query: GetUserChatsQueryParams = { limit: 50, includeMembers: true };
await sdk.getUserChats("user-uuid-123", query);updateChatRoom(chatId: UUID, updateData: { title?: string; description?: string }): Promise<ApiResponse>
Updates the metadata for a specific chat room.
Example Request:
await sdk.updateChatRoom("workspace-123", {
title: "New Team Title",
description: "Updated room description"
});Retrieves users from the chat service using the /v2/chats/users endpoint.
Interface: GetUsersQueryParams
interface GetUsersQueryParams {
chatName?: string; // string (optional): Filter by roomId (appId_roomId)
xmppUsername?: string; // string (optional): Filter by specific JID
}Example Request:
await sdk.getUsers({ chatName: "appId_workspace-123" });Limits: 1-100 users per request
Deletes users from the chat service using the /v1/users/batch endpoint.
Example Request:
await sdk.deleteUsers(["user-id-1", "user-id-2"]);Note: Gracefully handles non-existent users (422 status with "not found").
Deletes a chat room using the /v1/chats endpoint.
Example Request:
await sdk.deleteChatRoom("workspace-uuid-123");Note: Gracefully handles non-existent rooms (422 status with "not found").
Generates a chat room JID from a chat ID.
Parameters:
chatId(UUID): The unique identifier of the chatfull(boolean, optional): Whether to include the full JID domain (default: true)
Returns: The JID string
- Full:
{appId}_{chatId}@conference.xmpp.chat.ethora.com - Short:
{appId}_{chatId}
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
Important: Pass the same canonical user ID/UUID that your backend uses when creating that user in Ethora. Do not switch between different user-id formats when creating the user and then minting the client token.
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;
// Handle specific error cases
if (status === 422) {
// Validation error
console.error('Validation error:', errorData);
} else if (status === 401) {
// Authentication error
console.error('Authentication failed - check your credentials');
} else if (status === 404) {
// Resource not found
console.error('Resource not found');
} else {
// Other HTTP errors
console.error(`HTTP error ${status}:`, errorData);
}
} else {
// Non-HTTP errors
console.error('Unexpected error:', error);
}
throw error;
}
}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 room already exists, that's okay
if (
error.response?.status === 422 &&
(errorMessage.includes('already exist') ||
errorMessage.includes('already exists'))
) {
console.log('Chat room already exists, continuing...');
return; // Success - room exists
}
}
// Re-throw if it's a different error
throw error;
}
}The SDK provides a singleton instance. Reuse it rather than creating multiple instances:
// Good
const chatService = getEthoraSDKService();
// Avoid
const chatService1 = getEthoraSDKService();
const chatService2 = getEthoraSDKService(); // UnnecessaryCreate a service wrapper in your application:
// services/chatService.ts
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[]) {
// Your custom logic here
await this.service.createChatRoom(workspaceId);
// ... more setup logic
}
// Expose other methods as needed
getService() {
return this.service;
}
}
export default new ChatServiceWrapper();Validate environment variables on application startup:
// config/validateEnv.ts
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(', ')}`,
);
}
}
// Call on startup
validateEthoraConfig();Integrate with your existing logging system:
import { getEthoraSDKService } from '@ethora/sdk-backend';
import { logger } from './utils/logger'; // Your 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;
}
}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);
}Solution: Ensure all required environment variables are set:
ETHORA_CHAT_API_URL=https://api.chat.ethora.com
ETHORA_CHAT_APP_ID=your_app_id
ETHORA_CHAT_APP_SECRET=your_app_secretSolution: Verify your ETHORA_CHAT_APP_SECRET is correct and matches your app ID.
Solution: Handle idempotent operations gracefully:
try {
await chatService.createUser(userId);
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 422) {
// User already exists, continue
console.log('User already exists');
} else {
throw error;
}
}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.
Solution: Ensure you're using TypeScript 5.0+ and have proper type definitions:
npm install --save-dev typescript@^5.0.0- Review the API Reference for detailed method documentation
- Check out the Examples directory for complete integration examples
- See the Healthcare Insurance Demo for a real-world use case
For issues, questions, or contributions, please refer to the main README.md file.
The project uses strict TypeScript settings. See tsconfig.json for details.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
Apache 2.0
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