Server API
Full reference for @useceleris/server: createSigner, the claims surface, and connecting to Celeris from a backend.
npm install @useceleris/server
Runs on trusted servers only — Node.js, Bun and Deno. This package holds your signing secret and must never reach a browser or mobile bundle.
createSigner(options)
function createSigner(options: SignerOptions): Signer;
type SignerOptions = {
clientId: string; // no colon, no CR/LF
signingSecret: string;
clock?: () => number; // Unix ms; a test seam
};
type Signer = { sign(claims: SigningClaims): SignedCredentials };
type SignedCredentials = {
readonly payload: string;
readonly signature: string;
};
sign() is synchronous and deterministic for fixed inputs. It takes no cancellation signal and returns no promise.
import { createSigner } from "@useceleris/server";
const signer = createSigner({
clientId: process.env.CELERIS_CLIENT_ID!,
signingSecret: process.env.CELERIS_SIGNING_SECRET!,
});
const credentials = signer.sign({
channels: { kind: "restricted", references: ["room-42"] },
permissions: {
kind: "restricted",
segments: [{ segmentId: "chat", read: true, write: true }],
},
reference: "user-8317",
replay: { lookbackMs: 30_000 },
});
Pass payload and signature back to the browser unchanged. They are opaque — never parse, re-encode or cache them.
The claims surface
type SigningClaims = {
readonly channels: ChannelScope;
readonly permissions: SegmentPermissions;
readonly reference?: string;
readonly replay?: boolean | { readonly lookbackMs: number };
readonly allowEcho?: boolean;
};
| Field | Shape | Notes |
|---|---|---|
channels | { kind: "all" } or { kind: "restricted", references } | References are 1–255 chars of [a-zA-Z0-9_-], unique, at least one. An empty list is rejected, never read as "all". |
permissions | { kind: "all", read, write } or { kind: "restricted", segments } | Flags are explicit booleans. A restricted list may be empty, which grants nothing. |
reference | optional string | Identity label in message metadata and presence. Nonempty, no colon, no CR/LF. Omit to let the server generate one. |
replay | boolean or { lookbackMs } | Defaults to false. Lookback is 0–4294967295 ms. |
allowEcho | optional boolean | Defaults to false: a connection does not receive its own publishes. |
type SegmentClaim = {
readonly segmentId: string;
readonly read: boolean;
readonly write: boolean;
};
Unrestricted access is always an explicit opt-in:
signer.sign({
channels: { kind: "all" },
permissions: { kind: "all", read: true, write: false },
});
write: true but read: false is a member that receives nothing — no error, no warning, just silence. If a client "connects fine but never gets messages", check read first.createCredentialProvider(options)
When a backend is itself a realtime client — a worker publishing events, say — this bridges the signer to the client package's provider.
function createCredentialProvider(
options: CredentialProviderOptions
): CredentialProvider;
type CredentialProviderOptions = {
signer: Signer;
claims: (
request: CredentialRequest
) => SigningClaims | Promise<SigningClaims>;
};
| Option | Required | Meaning |
|---|---|---|
signer | Yes | A signer with a synchronous sign(claims) method |
claims | Yes | Server-owned authorization callback, called for each attempt |
The returned provider checks request.signal before and after awaiting claims, then signs. An aborted signal prevents signing; the synchronous signer itself is not interruptible. CredentialRequest is re-exported by the server package; see the client types for its fields.
import { createClient } from "@useceleris/client";
import { createCredentialProvider, createSigner } from "@useceleris/server";
const signer = createSigner({
clientId: process.env.CELERIS_CLIENT_ID!,
signingSecret: process.env.CELERIS_SIGNING_SECRET!,
});
const client = createClient({
credentialProvider: createCredentialProvider({
signer,
claims: (request) => ({
channels: { kind: "restricted", references: [request.channelReference] },
permissions: {
kind: "restricted",
segments: [{ segmentId: "chat", read: true, write: true }],
},
reference: "worker-1",
replay:
request.replayLookbackMs !== undefined
? { lookbackMs: Math.min(request.replayLookbackMs, 30_000) }
: false,
}),
}),
});
const channel = client.channel("room-42");
await channel.connect();
Your claims callback runs freshly on every attempt and is the sole authority over scope — nothing from the request is copied into the claims automatically. From there you use the ordinary client API to publish and subscribe.
This example's channel is chosen by trusted worker code. In an HTTP credential endpoint, authorize the requested channel against the verified user's access before signing it. The 30-second replay cap is an example application policy, not a retention promise. See Authentication and Reconnection and Recovery.
Fresh signing does not guarantee a particular credential lifetime or revoke an existing connection. Keep signed credentials confidential and pass them unchanged; the signer has no expiry-duration or revocation option.
Errors
Both failure modes carry a stable code. A Configuration message names each option or claim that failed and the rule it broke, such as Invalid claims. channels.references[0]: Must not be empty., and never echoes your input, secrets or claims. The error classes are deliberately not exported — identify failures by code.
code | Raised when |
|---|---|
Configuration | Invalid signer options or claims. Fix the call site. |
SigningFailed | Encoding or cryptographic failure during signing |
Both options objects are strict: an unexpected key is a Configuration error, not a silently ignored field.
This applies to signer and credential-provider options. Claims are validated separately and unknown claim keys are stripped; do not rely on an unknown key to grant access or enforce a restriction.