Docs menu

@dotwire/host

@dotwire/host is the Node.js SDK for your host backend. It mints RS256 tokens with your private key and calls dotwire's HTTP API, so your backend never needs direct database access. It requires Node.js 20 or later.

Keep it on the server. It holds your signing key.

Install

@dotwire/host on npm

npm install @dotwire/host

Quickstart

import { DotwireHostClient } from '@dotwire/host';

const host = new DotwireHostClient({
  baseUrl: 'http://localhost:8080',
  privateKeyPem: process.env.DOTWIRE_PRIVATE_KEY_PEM!,
  keyId: 'prod-key-1',
  issuer: 'my-app-auth',
  audience: 'dotwire'
});

const userToken = await host.mintToken('user_123', {
  role: 'member',
  displayName: 'Alice',
  ttl: '1h'
});

await host.setUserRole('user_123', 'member');

const room = host.room('a0000000-0000-0000-0000-000000000001');
await room.addMembers(['user_123', 'user_456']);

await room.sendSystemMessage('Welcome to the room!');

Configuration

OptionDefaultDescription
baseUrlRequireddotwire's base URL.
issuerRequiredThe iss claim. Must match dotwire's Auth:Issuer.
audience'dotwire'The aud claim. Must match dotwire's Auth:Audience.
keyIdRequiredThe kid header. Must match a key dotwire trusts.
privateKeyPemRequiredYour RS256 private key, PEM-encoded.
adminUserId'host-admin'User id the SDK signs admin calls as.
auditorUserId'host-auditor'User id the SDK signs audit calls as.
defaultTokenTtl'15m'Token lifetime as a duration string.
requestTimeoutMs15000Timeout for HTTP requests.
webhookSecret—HMAC secret for verifying X-Dotwire-Signature on webhook calls.
roleCacheTtlMs300000How long resolved roles stay cached.
presend, postSend—Moderation hooks. You can also assign them on the client later.
fetchGlobal fetchCustom fetch implementation.

Identity rules

  • Role, membership, injection, redaction, retention, and DSAR calls are signed as adminUserId. Migrations seed host-admin with the admin role.
  • Audit calls are signed as auditorUserId. Migrations seed host-auditor with the auditor role.
  • Calls made as a user, such as sendAsUser, getMessages, getRoomParticipants, and streamRoomEvents, mint a token for that user. The SDK tries member first. On a 403, it looks up the user's role through the admin API, caches it, and retries once. A second 403 throws a Forbidden error.
  • mintToken with an explicit role skips role resolution: you assert the role.

The SDK caches its own admin and auditor tokens and re-mints them one minute before they expire.

Tokens

// Standard member token
const token = await host.mintToken('alice', {
  role: 'member',
  displayName: 'Alice Smith',
  ttl: '1h'
});

// Auditor token (audit access; room history still requires room membership)
const auditorToken = await host.mintToken('auditor-dave', {
  role: 'auditor',
  ttl: '30m'
});

tokenResponse({ userId, role?, displayName?, ttl? }) returns a Fetch API Response with body { token }, ready to return from a route handler. If you omit role, the SDK looks it up and falls back to member for an unknown user.

const res = await host.tokenResponse({ userId: 'newbie' });

Roles and membership

// Assign or update a user's role
await host.setUserRole('alice', 'member');
await host.setUserRole('dave', 'auditor');

// Retrieve a user's role
const userRole = await host.getUserRole('alice');

// Revoke a user's role (also removes user from room memberships)
await host.deleteUserRole('alice');

const roomId = 'a0000000-0000-0000-0000-000000000001';

await host.addRoomMembers(roomId, ['alice', 'bob', 'charlie']);
const members = await host.getRoomMembers(roomId);
const participantIds = await host.getRoomParticipants(roomId, 'alice');
const userRooms = await host.getUserRooms('alice');
await host.removeRoomMember(roomId, 'bob');

addRoomMembers grants the member role to listed users who have no role yet, unless you pass { ensureMemberRole: false }. See Rooms and membership.

Room handles

host.room(roomId) returns a DotwireHostRoom with room-scoped versions of the same calls:

const room = host.room('a0000000-0000-0000-0000-000000000001');

await room.addMembers(['alice', 'bob']);
const members = await room.getMembers();
await room.removeMember('bob');

await room.sendSystemMessage('Server restart scheduled in 5 minutes.');
await room.sendAsUser('alice', 'Hello');

const history = await room.getMessages({ asUserId: 'alice', limit: 50 });

The handle also has getParticipants(asUserId), events(options) for SSE, and redact(seq).

Messages

  • sendAsUser(roomId, userId, content) sends as a room member. It runs your presend and postSend hooks.
  • sendSystemMessage(roomId, content, { senderId? }) injects a message through the admin API. The sender defaults to system. Hooks don't run.
  • getMessages(roomId, { asUserId, afterSeq?, beforeSeq?, limit? }) reads history as that user.

Both send methods resolve with { seq, time }.

Streaming events (SSE)

streamRoomEvents(roomId, { asUserId, afterSeq?, autoResume?, signal? }) is an async iterable of room events over SSE.

for await (const evt of host.streamRoomEvents(roomId, { asUserId: 'dave' })) {
  // evt.type is 'message', 'retracted', 'typing', 'presence', or 'revoked'
}

With autoResume on (the default), a dropped connection reconnects with Last-Event-ID set to the last message seq, backing off exponentially from 1 second to 30 seconds. A revoked event ends the stream and never triggers a reconnect.

Compliance

const result = await host.redactMessage('room1', 7);

await host.setMessageRetentionDays(90);
const days = await host.getMessageRetentionDays();

const exported = await host.exportUser('dave');
const stream = await host.exportUserStream('dave');

const page = await host.readAuditLog(0, 100);
const verification = await host.verifyAuditLog(true);
const checkpoints = await host.getAuditCheckpoints();

See Encryption and retention and Audit log.

Moderation and webhooks

The host client carries the presend and postSend hooks and the handlers handleClientSend, handleClientSendBody, handlePresendWebhook, handlePresendWebhookBody, handlePostSendWebhook, and handlePostSendWebhookBody. The package also exports ModerationQueue and BatchModerationWorker. See Moderation webhooks.

Errors

  • DotwireApiError is thrown when dotwire answers with a non-success status. It has status, method, path, optional code, reason, and retryAfterMs, and a kind of BadRequest, Unauthorized, Forbidden, NotFound, PresendRejected, RateLimited, Unavailable, or Unknown.
  • PresendRejectedError is thrown by sendAsUser when your presend hook rejects a message.
  • PostSendError is thrown by sendAsUser when your postSend hook throws. It carries the ack, because the message was already sent.

Related