Documentation

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.

Use your signing secret, never a client secret, and keep it in server-side configuration. Anyone holding it can impersonate any of your users.

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;
};
FieldShapeNotes
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.
referenceoptional stringIdentity label in message metadata and presence. Nonempty, no colon, no CR/LF. Omit to let the server generate one.
replayboolean or { lookbackMs }Defaults to false. Lookback is 0–4294967295 ms.
allowEchooptional booleanDefaults 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 },
});
A token with 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>;
};
OptionRequiredMeaning
signerYesA signer with a synchronous sign(claims) method
claimsYesServer-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.

codeRaised when
ConfigurationInvalid signer options or claims. Fix the call site.
SigningFailedEncoding 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.

We use Google Analytics cookies to understand how people use Celeris, only if you allow it. See our Cookie Policy.