Skip to content
Server SDK

Documentation

Server SDK

Sign chat tokens and run admin operations from your backend with @emb-chat/server-sdk.

@emb-chat/server-sdk is the backend half of emb.chat. It does two things: ChatAuth signs the tokens your users connect with, and ChatAdmin performs host-side admin operations over REST with your API key.

npm install @emb-chat/server-sdk
import { ChatAuth, ChatAdmin, ChatApiError } from '@emb-chat/server-sdk';

ChatAuth — signing tokens

Tokens are signed with RS256 or EdDSA (asymmetric only — HS* and none are rejected). You hold the private key; emb.chat verifies with the public key you registered under a kid.

Generate a keypair

const pair = await ChatAuth.generateKeyPair('RS256'); // or 'EdDSA'
// pair.privatePem  -> keep on your backend, never commit
// pair.publicPem   -> register with emb.chat under a kid

Load a private key

import { readFileSync } from 'node:fs';

const privateKey = await ChatAuth.loadPrivateKey(
  readFileSync('./emb-private.pem', 'utf8'),
);

Create a token

const token = await ChatAuth.createToken(privateKey, {
  sub: 'user_abc123',   // required — your user id
  aud: 'app_demo',      // required — your app id
  kid: 'key_01',        // required — selects the registered public key
  name: 'Jane Doe',     // optional — mirrored to display_name
  avatar: 'https://…',  // optional — mirrored to avatar_url
  meta: { role: 'admin' }, // optional — mirrored to metadata
  expiresIn: '1h',
});
ClaimRequiredPurpose
subThe host’s user id.
audThe app id — selects which registered keys verify the token.
kidKey id in the JWT header; looks up the public key by (aud, kid).
expSet via expiresIn. 15m1h for browsers, 24h for backend/mobile.
name / avatar / metaMirrored into the user cache on connect (field-conditional — omitting them never wipes existing values).

Rotate a key

Register the new key, start signing with its kid, then deactivate the old one. Tokens already signed with the old kid keep verifying until they expire.

await admin.keys.register({ kid: 'key_02', publicKey: pair.publicPem, algorithm: 'RS256' });
// …sign new tokens with kid: 'key_02'…
await admin.keys.delete('key_01');

ChatAdmin — admin operations

ChatAdmin authenticates with your app API key (Authorization: Bearer <api_key>) and exposes the full host-side REST surface. API-key callers bypass the group-mode matrix — they always have full rights for that app.

const admin = new ChatAdmin({
  url: 'https://chat.acme.com',
  apiKey: process.env.EMB_CHAT_API_KEY!, // never in source control
  appId: 'app_demo',
});

Users

await admin.users.upsert({ id: 'user_abc123', display_name: 'Jane Doe' });
const presence = await admin.users.presence('user_abc123'); // { user_id, status: 'online' | 'offline' }

Channels

const channel = await admin.channels.create({
  type: 'group',
  name: 'Engineering',
  members: ['user_abc123', 'user_xyz789'],
});

await admin.channels.update(channel.id, { name: 'Eng' });
await admin.channels.sendMessage(channel.id, { body: 'Welcome!' });

const members = await admin.channels.members(channel.id);
await admin.channels.setMemberRole(channel.id, 'user_xyz789', 'admin');

const channels = await admin.channels.list();
await admin.channels.delete(channel.id);

Apps & group mode

const app = await admin.apps.get();
await admin.apps.update({ config: { group_mode: 'hybrid' } }); // 'external' | 'internal' | 'hybrid'

Webhooks

const wh = await admin.webhooks.register({
  url: 'https://acme.com/hooks/chat',
  events: ['message.*', 'member.*', 'presence.*'],
});
// wh.secret is returned exactly once — store it now.

await admin.webhooks.list();
await admin.webhooks.update(wh.id, { events: ['message.created'] });
await admin.webhooks.deliveries(wh.id);  // per-endpoint delivery log
await admin.webhooks.test(wh.id);        // fire a one-shot test event
await admin.webhooks.delete(wh.id);

See the Webhooks reference for the delivery envelope and verification.

Error handling

Every failed REST call throws ChatApiError — one type for both SDKs. Branch on status and the stable code.

import { ChatApiError } from '@emb-chat/server-sdk';

try {
  await admin.channels.delete(channelId);
} catch (err) {
  if (err instanceof ChatApiError && err.code === 'channel_not_found') {
    return; // already gone — ignore
  }
  throw err;
}
class ChatApiError extends Error {
  readonly status: number; // HTTP status (0 for transport-only failures)
  readonly code: string;   // stable machine code, e.g. 'channel_not_found'
  readonly message: string;
  readonly raw: unknown;   // parsed response body
}

Common code values: validation_error, channel_not_found, forbidden_in_group_mode, role_required, last_owner_required, invalid_token, expired_token, unknown_kid, inactive_key, invalid_api_key.