Docs menu

Dotwire.Host

Dotwire.Host is the .NET SDK for your host backend. It mints RS256 tokens with your private key and calls dotwire's HTTP API. It targets net8.0, net9.0, and net10.0, and depends only on Microsoft.IdentityModel.JsonWebTokens and Microsoft.IdentityModel.Tokens.

There is no .NET member client. Browsers use @dotwire/client.

Install

dotnet add package Dotwire.Host

Quickstart

var options = new DotwireHostOptions
{
    BaseUrl = new Uri("http://localhost:8080"),
    Issuer = "my-host",
    KeyId = "host-key-1",
    PrivateKeyPem = File.ReadAllText("host-jwt.key"),
};
var http = new HttpClient(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2) })
{
    BaseAddress = options.BaseUrl,
};
IDotwireHostClient client = new DotwireHostClient(http, new DotwireTokenSigner(options), options);

var ack = await client.SendAsUserAsync(roomId, "user-42", "hello");
// ack.Seq is the JetStream ordering token - the handle a later redaction targets.

var participantIds = await client.GetRoomParticipantsAsync(roomId, "user-42");
// The caller must be a room member. Resolve names and avatars in the host app.

Options

DotwireHostOptions properties:

PropertyDefaultDescription
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.
DefaultTokenTtl15 minutesLifetime of tokens the SDK mints internally.
RequestTimeout15 secondsTimeout for HTTP requests.
WebhookSecret—HMAC secret for verifying X-Dotwire-Signature on webhook calls.
RoleCacheTtl5 minutesHow long resolved roles stay cached.

Dependency injection

The SDK doesn't reference Microsoft.Extensions.DependencyInjection.Abstractions or Microsoft.Extensions.Configuration.Abstractions, so it ships no AddDotwireHost extension method. To register it in a service collection, add those two packages to your project and write the registration yourself. The Dotwire.Host README in the repository has a complete extension method you can copy. Its configuration keys match the DotwireHostOptions property names.

Identity rules

  • Role, membership, injection, redaction, DSAR, and retention calls are signed as AdminUserId. Migrations seed host-admin with the admin role.
  • Audit read, verify, and checkpoint calls are signed as AuditorUserId. Migrations seed host-auditor with the auditor role.
  • Send-as-user, history-as-user, SSE-as-user, and HandleClientSendAsync mint a token for that user. The SDK tries member first. On a 403, it looks the role up through GET /admin/users/{userId}/role, caches it for RoleCacheTtl, and retries once. A second 403 throws DotwireApiException with Kind == Forbidden.
  • MintToken(userId, role, ...) skips role resolution: you assert the role.

Admin and auditor tokens are cached and re-minted one minute before they expire.

Methods

IDotwireHostClient exposes:

AreaMethods
TokensMintToken(userId, role, displayName, ttl)
RolesSetUserRoleAsync, GetUserRoleAsync, DeleteUserRoleAsync, GetUserRoomsAsync
MembershipAddRoomMembersAsync(roomId, userIds, ensureMemberRole = true), RemoveRoomMemberAsync, GetRoomMembersAsync, GetRoomParticipantsAsync(roomId, asUserId)
MessagesSendAsUserAsync(roomId, userId, content), SendSystemMessageAsync(roomId, content, senderId = "system"), GetMessagesAsync(roomId, asUserId, query)
ComplianceRedactMessageAsync, ExportUserAsync, GetMessageRetentionDaysAsync, SetMessageRetentionDaysAsync
AuditReadAuditLogAsync(afterId, limit), VerifyAuditLogAsync(full), GetAuditCheckpointsAsync(limit)
EventsStreamEventsAsync(roomId, asUserId, options)
HandlersHandleClientSendAsync, HandlePresendWebhookAsync, HandlePostSendWebhookAsync
HooksPresend, PostSend

Every async method takes an optional CancellationToken. Room ids are Guid values.

Membership

Guid roomId = Guid.Parse("a0000000-0000-0000-0000-000000000001");
await client.SetUserRoleAsync("user-42", "member");
await client.AddRoomMembersAsync(roomId, ["user-42", "user-57"]);

string token = client.MintToken("user-42", role: "member");
IReadOnlyList<string> userIds = await client.GetRoomParticipantsAsync(roomId, "user-42");

GetRoomMembersAsync(roomId) is an admin operation for host management. GetRoomParticipantsAsync(roomId, asUserId) uses that user's room access.

System messages

var ack = await client.SendSystemMessageAsync(roomId, "system announcement");
var botAck = await client.SendSystemMessageAsync(roomId, "build finished", senderId: "bot:ci");

The admin identity doesn't need to be a room member, and every injection is audited. If the audit event can't be written, dotwire erases the message before anyone receives it and the call throws DotwireApiException with Kind == Unavailable, so retrying is safe. See System messages.

Streaming events (SSE)

await foreach (var evt in client.StreamEventsAsync(roomId, asUserId: "user-42"))
{
    switch (evt)
    {
        case MessageEvent m: Render(m); break;
        case RetractedEvent r: Remove(r.Seq); break;
        case TypingEvent t: break;
        case PresenceEvent p: break;
        case RevokedEvent: return; // membership revoked - server closed the stream, we stop
    }
}

Render and Remove stand for your own code. With StreamOptions.AutoResume = true (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, and mints a fresh token for each attempt. A RevokedEvent ends the stream and never triggers a reconnect.

Compliance

var result = await client.RedactMessageAsync(roomId, seq);
// result.RemovedFromHistory, result.RemovedFromStream

var page = await client.ReadAuditLogAsync(afterId: 0, limit: 100);
var verification = await client.VerifyAuditLogAsync(full: true);
var checkpoints = await client.GetAuditCheckpointsAsync();

await client.SetMessageRetentionDaysAsync(365); // 0 = indefinite
var days = await client.GetMessageRetentionDaysAsync();

var export = await client.ExportUserAsync(userId);          // buffered UserExport
await client.ExportUserAsync(userId, someStream);            // streamed straight through

See Encryption and retention and Audit log.

Moderation and webhooks

Presend and PostSend run for SendAsUserAsync, HandleClientSendAsync, and the webhook handlers, but never for SendSystemMessageAsync. The SDK also includes ModerationQueue and BatchModerationWorker in Dotwire.Host.Moderation. See Moderation webhooks.

Errors

  • DotwireApiException carries Kind (BadRequest, Unauthorized, Forbidden, NotFound, PresendRejected, RateLimited, Unavailable, or Unknown), the ErrorCode and Reason from the error body when present, and RetryAfter, parsed from the Retry-After header on a 429.
  • PresendRejectedException is thrown by SendAsUserAsync when your Presend hook rejects a message.
  • PostSendException is thrown by SendAsUserAsync when your PostSend hook throws. It carries the ack. The message was already sent, so don't retry it.

Related