@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
npm install @dotwire/hostQuickstart
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
| Option | Default | Description |
|---|---|---|
baseUrl | Required | dotwire's base URL. |
issuer | Required | The iss claim. Must match dotwire's Auth:Issuer. |
audience | 'dotwire' | The aud claim. Must match dotwire's Auth:Audience. |
keyId | Required | The kid header. Must match a key dotwire trusts. |
privateKeyPem | Required | Your 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. |
requestTimeoutMs | 15000 | Timeout for HTTP requests. |
webhookSecret | — | HMAC secret for verifying X-Dotwire-Signature on webhook calls. |
roleCacheTtlMs | 300000 | How long resolved roles stay cached. |
presend, postSend | — | Moderation hooks. You can also assign them on the client later. |
fetch | Global fetch | Custom fetch implementation. |
Identity rules
- Role, membership, injection, redaction, retention, and DSAR calls are signed as
adminUserId. Migrations seedhost-adminwith theadminrole. - Audit calls are signed as
auditorUserId. Migrations seedhost-auditorwith theauditorrole. - Calls made as a user, such as
sendAsUser,getMessages,getRoomParticipants, andstreamRoomEvents, mint a token for that user. The SDK triesmemberfirst. On a403, it looks up the user's role through the admin API, caches it, and retries once. A second403throws aForbiddenerror. mintTokenwith an explicitroleskips 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 yourpresendandpostSendhooks.sendSystemMessage(roomId, content, { senderId? })injects a message through the admin API. The sender defaults tosystem. 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
DotwireApiErroris thrown when dotwire answers with a non-success status. It hasstatus,method,path, optionalcode,reason, andretryAfterMs, and akindofBadRequest,Unauthorized,Forbidden,NotFound,PresendRejected,RateLimited,Unavailable, orUnknown.PresendRejectedErroris thrown bysendAsUserwhen yourpresendhook rejects a message.PostSendErroris thrown bysendAsUserwhen yourpostSendhook throws. It carries the ack, because the message was already sent.
Related
- Integrate a host app
Dotwire.Host, the .NET equivalent