Documentation

Server-side Usage

Use a trusted Node process to sign credentials, publish updates, and receive realtime messages.

Before you start: complete the Quickstart and keep its browser subscriber connected. Use the same Celeris app credentials. This example runs only on your trusted backend, never in the browser.

1. Understand the two responsibilities

@useceleris/server signs access. It does not expose a separate publishing transport. Your backend uses @useceleris/client to connect and publish, just as a browser does, but it can obtain credentials locally through createCredentialProvider().

Backend signer -> backend client -> Celeris -> browser subscriber

For browser authentication, keep using an authenticated HTTP credential endpoint instead; see Authentication.

2. Create a worker

Both packages are already installed by the Quickstart. Add worker.mjs to that project's root:

worker.mjs
import {
  createClient,
  readText,
  textPayload,
  ServerError,
} from "@useceleris/client";
import { createSigner, createCredentialProvider } from "@useceleris/server";

const { CELERIS_CLIENT_ID, CELERIS_SIGNING_SECRET } = process.env;
if (!CELERIS_CLIENT_ID || !CELERIS_SIGNING_SECRET) {
  throw new Error("Set both Celeris values in the server .env file.");
}

const signer = createSigner({
  clientId: CELERIS_CLIENT_ID,
  signingSecret: CELERIS_SIGNING_SECRET,
});
const client = createClient({
  credentialProvider: createCredentialProvider({
    signer,
    claims: (request) => {
      if (request.channelReference !== "demo-room") {
        throw new Error("Worker channel is not allowed.");
      }
      return {
        channels: { kind: "restricted", references: ["demo-room"] },
        permissions: {
          kind: "restricted",
          segments: [{ segmentId: "chat", read: true, write: true }],
        },
        reference: "demo-worker",
        allowEcho: false,
        replay:
          request.replayLookbackMs === undefined
            ? false
            : { lookbackMs: Math.min(request.replayLookbackMs, 30_000) },
      };
    },
  }),
});

const channel = client.channel("demo-room");
const chat = channel.segment("chat");
const stopErrors = channel.events().onError((error) => {
  console.error(
    error instanceof ServerError ? error.type : error.code,
    error.message
  );
});
const stopMessages = chat.onMessage((payload) =>
  console.log("Received:", readText(payload))
);
const stopRecovery = channel.events().onRecovery(() => {
  console.log(
    "Recovered; reload authoritative state if your application needs it."
  );
});
const membership = chat.subscribe();

async function shutdown() {
  await channel.close();
  membership.cancel();
  stopMessages();
  stopRecovery();
  stopErrors();
}

process.once("SIGINT", () => {
  void shutdown().catch(() => {
    process.exitCode = 1;
  });
});
process.once("SIGTERM", () => {
  void shutdown().catch(() => {
    process.exitCode = 1;
  });
});

try {
  await channel.connect();
  await chat.publish({ payload: textPayload("Hello from the backend") });
  console.log(
    "Handed to the socket. Send a browser reply, or press Ctrl+C to close."
  );
} catch (error) {
  console.error(error.code ?? "Error", error.message);
  await shutdown();
  process.exitCode = 1;
}

The claims callback is the authority. Nothing automatically turns the request into permission. This worker explicitly restricts its channel and segment and caps requested replay at 30 seconds.

3. Run and verify

From the project root:

node --env-file=.env worker.mjs

The connected browser should receive demo-worker: Hello from the backend. Send a browser reply; the worker should print Received: followed by the text. Press Ctrl+C to close it.

The worker logs local send completion, not confirmed delivery. If the browser was not connected or subscribed when it sent, that publish is not automatically resent.

4. Apply this to your service

Sign from server-owned configuration. For multi-tenant services, authorize each channel instead of replacing the restriction with unrestricted claims.

The provider calls your claims callback on every attempt and signs freshly. It honors cancellation before signing; signer.sign() itself is synchronous and takes no abort signal. The server package has no socket to dispose; the client channel does.

Use Reconnection and Recovery to decide how your service reconciles missed or duplicate updates. Publishing has the same local-acceptance semantics on servers as it does in browsers.

Next: Server API and Client API.

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