Skip to content

dappros/ethora-sdk-backend-integration

Repository files navigation

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.

Part of the Ethora SDK ecosystem — see all SDKs, tools, and sample apps. Follow cross-SDK updates in the Release Notes.

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

API Documentation (Swagger)

Ethora exposes Swagger UI from every running backend instance at:

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).

Tenant Admin / B2B Endpoints

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.

Token Types

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 via x-custom-token, and many deployments also accept it in Authorization: Bearer ....
  • Client JWT: created by createChatUserJwtToken(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 explicit appId, 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.

API Versioning

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 appId request context rather than relying on implicit app-token scope.

Installation

Step 1: Install the Package

npm install @ethora/sdk-backend
# or
yarn add @ethora/sdk-backend
# or
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:

# 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

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';

// Load environment variables
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)

// services/chatService.ts
import { getEthoraSDKService } from '@ethora/sdk-backend';

// Get the singleton instance
const chatService = getEthoraSDKService();

export default chatService;

Option B: Direct Initialization

// In your route handler or service
import { getEthoraSDKService } from '@ethora/sdk-backend';

const chatService = getEthoraSDKService();

Option C: Dependency Injection (for frameworks like NestJS)

// 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
}

Integration Patterns

Pattern 1: Express.js Integration

// 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;

Pattern 2: NestJS Integration

// 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) };
  }
}

Pattern 3: Fastify Integration

// 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 };
    },
  );
}

Common Use Cases

Use Case 0: Tenant-admin / child-app management

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']);

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,
): 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;
  }
}

Use Case 2: User Onboarding

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;
  }
}

Use Case 3: Adding User to Existing Workspace

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;
  }
}

Use Case 4: Removing User from Workspace

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;
  }
}

Use Case 5: Cleanup on Workspace Deletion

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;
  }
}

Use Case 6: Getting Users

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;
  }
}

Use Case 7: Updating Users (Batch)

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;
  }
}

Use Case 8: Updating Chat Room Metadata

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;
  }
}

Use Case 9: Getting User Chats

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;
  }
}

API Reference

Tenant-admin methods

createApp(appData: CreateAppRequest): Promise<ApiResponse>

Creates a child app through POST /v2/apps.

createUsersInApp(appId: UUID, payload: BatchCreateUsersRequest): Promise<ApiResponse>

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.

deleteChatRoomInApp(appId: UUID, chatId: UUID): Promise<ApiResponse>

Deletes a chat in a target app through DELETE /v2/apps/{appId}/chats.

Core Methods

createUser(userId: UUID, userData?: CreateUserData): Promise<ApiResponse>

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".


createChatRoom(chatId: UUID, roomData?: CreateChatRoomRequest): Promise<ApiResponse>

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);

grantUserAccessToChatRoom(chatId: UUID, userId: UUID | UUID[]): Promise<ApiResponse>

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.


removeUserAccessFromChatRoom(chatId: UUID, userId: UUID | UUID[]): Promise<ApiResponse>

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.


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.

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" }
]);

getUserChats(userId: UUID, params?: GetUserChatsQueryParams): Promise<ApiResponse>

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"
});

getUsers(params?: GetUsersQueryParams): Promise<ApiResponse>

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


deleteUsers(userIds: UUID[]): Promise<ApiResponse>

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").


deleteChatRoom(chatId: UUID): Promise<ApiResponse>

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").


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.chat.ethora.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

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.


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;

      // 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;
  }
}

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 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;
  }
}

Best Practices

1. Use Singleton Pattern

The SDK provides a singleton instance. Reuse it rather than creating multiple instances:

// Good
const chatService = getEthoraSDKService();

// Avoid
const chatService1 = getEthoraSDKService();
const chatService2 = getEthoraSDKService(); // Unnecessary

2. Centralize Chat Service

Create 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();

3. Environment Variable Validation

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();

4. Logging Integration

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;
  }
}

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.chat.ethora.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) {
    // User already exists, continue
    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

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests if applicable
  5. 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

About

Documentation and code samples for integrating your own backend or implementing advanced business logic with Ethora platform

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors