Documentation

Authentication

Replace the local demo identity with server-verified users, restricted permissions, and fresh credentials.

Start with the Quickstart. This guide explains what must change before deploying its credential endpoint.

Celeris verifies signed access claims. Your application authenticates the user and decides those claims. A channel name sent by the browser is a request for access, not evidence that access is allowed.

1. Verify the user on your server

Use your application's existing session or access-token verification. Do not take a user ID, role, room list, or permission flags from the credential request body.

The flow is:

Browser -> your credential endpoint -> verify session
                                   -> authorize requested channel
                                   -> choose segment permissions
                                   -> sign credentials
Browser <- { payload, signature }
Browser -> Celeris using those credentials

If the session is missing or invalid, return 401. If the user cannot access the requested channel, return 403. Use HTTPS outside local development, your authentication system's CSRF protection, appropriate request-size limits, and rate limiting.

2. Sign only authorized access

The following is an integration pattern, not a standalone server. getUserFromSession, canReadRoom, and canWriteRoom represent your application's verified session and authorization checks. Implement them using server-owned data before using this handler.

credential-handler.js
import { createSigner } from "@useceleris/server";

const signer = createSigner({
  clientId: process.env.CELERIS_CLIENT_ID,
  signingSecret: process.env.CELERIS_SIGNING_SECRET,
});

export async function handleCredentials(request) {
  const user = await getUserFromSession(request);
  if (!user) return Response.json({ error: "Sign in first." }, { status: 401 });

  let body;
  try {
    body = await request.json();
  } catch {
    return Response.json({ error: "Invalid JSON." }, { status: 400 });
  }

  const channelReference = body?.channelReference;
  if (
    typeof channelReference !== "string" ||
    !/^[a-zA-Z0-9_-]{1,255}$/.test(channelReference)
  ) {
    return Response.json(
      {
        error:
          "Invalid channel. Use 1–255 ASCII letters, digits, hyphens (-) or underscores (_).",
      },
      { status: 400 },
    );
  }
  if (!(await canReadRoom(user, channelReference))) {
    return Response.json({ error: "Not allowed." }, { status: 403 });
  }

  const mayWrite = await canWriteRoom(user, channelReference);
  const credentials = signer.sign({
    channels: { kind: "restricted", references: [channelReference] },
    permissions: {
      kind: "restricted",
      segments: [{ segmentId: "chat", read: true, write: mayWrite }],
    },
    reference: user.realtimeReference,
    replay: false,
    allowEcho: false,
  });

  return Response.json(credentials, {
    headers: { "cache-control": "no-store" },
  });
}

Use a server-owned, stable realtimeReference that is safe to expose to peers. It must be nonempty and contain no colon, CR, or LF. Do not use an email address or private account data just because it is convenient.

Configure your framework to bound request bodies and convert unexpected failures to a generic error response without exposing secrets or claims. The Quickstart already checks that environment values are present; do the same at your production server's startup.

What the claims mean

ClaimPurpose
channelsChannel references this connection may use
permissionsSegment read/write access
referenceIdentity label peers receive in message metadata and presence
replayWhether to request recent retained messages on segment join
allowEchoWhether this connection receives its own publishes; default false

Use restricted channels and segments by default. An empty channel-reference list is invalid; an empty restricted segment list grants no segment permissions. Neither means "all."

Connecting automatically joins the channel's default segment, but membership does not grant read/write access. To use it, include { segmentId: "default", read: true, write: true } in your server-owned segment permissions. Grant only the operations the user needs. The example above grants chat instead, so it does not authorize default-segment messaging.

A write-only member can publish but receives no messages. Read access is evaluated at join, so a connected socket alone does not prove that a subscription is readable.

3. Decide whether to allow replay

During recovery the client's credential request includes replayLookbackMs. Your browser provider must forward it to your endpoint if the endpoint needs it:

// Inside the credential provider's fetch options:
body: JSON.stringify({
  channelReference: request.channelReference,
  replayLookbackMs: request.replayLookbackMs,
});

Treat this as untrusted input, just like the requested channel. The server decides whether that user may receive history and how much to request.

For example, after authorizing history access, replace replay: false with this policy:

// Example policy: request at most 30 seconds; this is not a retention guarantee.
const requestedLookback = body.replayLookbackMs;
const replay =
  Number.isInteger(requestedLookback) && requestedLookback > 0
    ? { lookbackMs: Math.min(requestedLookback, 30_000) }
    : false;

Use the resulting replay in signer.sign(). Users without history permission should still receive replay: false. Never copy the entire request into signing claims.

The server replays retained data per segment join. A signed lookback does not guarantee that all requested history exists. See Reconnection and Recovery.

4. Fetch fresh credentials for every attempt

The client calls its provider for the first connection and every automatic reconnect. Sign freshly for every request; do not cache or backdate credentials.

The signer does not expose an expiry-duration option, automatic revocation API, or a guarantee that a freshly signed credential has a particular short lifetime. Fresh signing does not shorten the server's acceptance window. Rechecking permissions on a new credential request is not immediate revocation of an existing connection.

Signed credentials are sensitive access material even though they do not reveal your signing secret. Anyone holding them may attempt to use the access they grant. Pass payload and signature through unchanged, use HTTPS, return Cache-Control: no-store, and do not log, persist, or share them.

5. Check the production boundary

Before deploying:

  • Replace the fixed demo identity with verified sessions.
  • Authorize every requested channel and choose permissions server-side.
  • Keep the signing secret out of browser/mobile bundles and public environment variables.
  • Do not import @useceleris/server into browser code.
  • Add CSRF protection as appropriate to your session scheme, rate limits, body limits, and safe failure responses.
  • Decide whether replay is allowed and cap requested history according to your policy.
  • Test denied access as well as the successful path.

Next: Connecting and lifecycle, or Server-side usage for a backend that consumes realtime itself.

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