Documentation

Java Server API

Full reference for celeris-server: Signer, the claims surface, and how a Java backend connects to Celeris.

com.useceleris:celeris-server:1.0.0

The artifact requires Java 17 or newer and runs on trusted servers only. The module and package are com.useceleris.server. This artifact holds your signing secret and must never ship in a browser, mobile or desktop application. It brings in celeris-client for the credential types and errors, and it signs with the JDK alone. On the module path, requires com.useceleris.server; reads the client too.

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

Signer.create()

public final class Signer {
  public static Signer create(SignerOptions options);
  public Credentials sign(Claims claims);
  public CredentialProvider credentialProvider(ClaimsFunction claimsFunction);
}

public final class SignerOptions {
  public static Builder builder();

  public static final class Builder {
    public Builder clientId(String clientId); // no colon, CR or LF
    public Builder signingSecret(String signingSecret);
    public Builder clock(Clock clock); // unset or null means the system UTC clock; a test seam
    public SignerOptions build();
  }
}

sign is synchronous and deterministic for fixed inputs. It stamps the clock's current time and returns the client's Credentials. A Signer is safe for concurrent use and holds no resources. Create one and share it. It keeps the secret only inside the key that its digest uses, so a printed Signer never shows the secret. SignerOptions and their builder print as redacted.

Signer signer =
    Signer.create(
        SignerOptions.builder()
            .clientId(System.getenv("CELERIS_CLIENT_ID"))
            .signingSecret(System.getenv("CELERIS_SIGNING_SECRET"))
            .build());

Credentials credentials =
    signer.sign(
        Claims.builder()
            .channels(ChannelScope.restricted("room-42"))
            .permissions(SegmentPermissions.restricted(new SegmentClaim("chat", true, true)))
            .reference("user-8317")
            .replay(Replay.lookback(Duration.ofSeconds(30)))
            .build());

Return credentials to the application unchanged. Jackson and Gson write it as {"payload": ..., "signature": ...}. Both values are opaque. Never parse, re-encode or cache them.

The claims surface

public final class Claims {
  public static Builder builder();

  public static final class Builder {
    public Builder channels(ChannelScope channels);
    public Builder permissions(SegmentPermissions permissions);
    public Builder reference(String reference);
    public Builder replay(Replay replay);
    public Builder allowEcho(boolean allowEcho);
    public Claims build();
  }
}

public final class ChannelScope {
  public static ChannelScope all();
  public static ChannelScope restricted(String... references);
}

public final class SegmentPermissions {
  public static SegmentPermissions all(boolean read, boolean write);
  public static SegmentPermissions restricted(SegmentClaim... segments);
}

public record SegmentClaim(String segmentId, boolean read, boolean write) {}

public final class Replay {
  public static Replay none();
  public static Replay backlog();
  public static Replay lookback(Duration lookback);
}
Builder methodValuesNotes
channelsChannelScope.all() or ChannelScope.restricted(references...)Required. References are 1 to 255 ASCII letters, digits, - or _, unique, at least one. The builder refuses an empty scope and never reads it as "all".
permissionsSegmentPermissions.all(read, write) or SegmentPermissions.restricted(claims...)Required. A restricted list can be empty, which grants nothing. Segment ids are unique, non-empty, no CR or LF.
referenceStringIdentity label in message metadata and presence. Non-empty, no colon, CR or LF. Leave it unset to let the server generate one.
replayReplay.none(), Replay.backlog() or Replay.lookback(duration)Defaults to Replay.none(). Applies each time the connection joins a segment. A lookback is whole milliseconds, 0 to 4294967295 ms. Replay.lookback(Duration.ZERO) replays nothing older than the join, which differs from Replay.none().
allowEchobooleanDefaults to false: a connection does not receive its own publishes.

ChannelScope, SegmentPermissions and Replay are opaque. Make them only with these factories. build() checks every rule, so claims that exist are valid. Every signed string must be well-formed UTF-16, without unpaired surrogates. Unrestricted access is always an explicit opt-in:

Credentials unrestricted =
    signer.sign(
        Claims.builder()
            .channels(ChannelScope.all())
            .permissions(SegmentPermissions.all(true, false))
            .build());
A token with write but not read can publish and cannot subscribe. The server refuses its subscription with a PermissionDeniedError. On default, such a token receives nothing, with no error. If a client connects but never gets messages, check read first.

Signer.credentialProvider()

When a backend is itself a realtime client, for example a worker that publishes events, this method bridges the signer to the client's provider.

public CredentialProvider credentialProvider(ClaimsFunction claimsFunction)

public interface ClaimsFunction {
  CompletionStage<Claims> claimsFor(CredentialRequest request);
}
ArgumentRequiredMeaning
claimsFunctionYesServer-owned authorization function, called for each attempt. Its stage yields the claims

The provider calls claimsFor(request) on every attempt, on the thread that asks for credentials, and then signs with a fresh timestamp. A cancel of the attempt also cancels the claims stage. The signer never signs claims that arrive after cancellation. A failure of the claims function, a throw or a failed stage, fails the provider's stage unchanged. See the client types for CredentialRequest's fields.

CredentialProvider provider =
    signer.credentialProvider(
        request -> {
          Claims.Builder claims =
              Claims.builder()
                  .channels(ChannelScope.restricted(request.channelReference()))
                  .permissions(SegmentPermissions.restricted(new SegmentClaim("chat", true, true)))
                  .reference("worker-1");

          // Present only on reconnect; an example policy capped at 30 seconds.
          request
              .replayLookback()
              .ifPresent(
                  lookback ->
                      claims.replay(
                          Replay.lookback(
                              lookback.compareTo(Duration.ofSeconds(30)) > 0
                                  ? Duration.ofSeconds(30)
                                  : lookback)));

          return CompletableFuture.completedFuture(claims.build());
        });

CelerisClient client = CelerisClient.create(ClientOptions.builder(provider).build());

try (Channel channel = client.channel("room-42")) {
  channel.connect().join();
}

Your claims function runs again on every attempt and is the sole authority over scope. The provider copies nothing from the request into the claims automatically. Trusted worker code chooses this example's channel. In an HTTP credential endpoint, authorize the requested channel against the verified user's access before you sign it. The 30-second replay cap is an example policy, not a retention promise.

The client reports a claims function that fails, or claims that do not build, as its generic TRANSPORT error. That error never repeats what went wrong. To check a claims function, build its claims directly. Invalid claims throw a CONFIGURATION that names the claim and the rule.

A fresh signature does not guarantee a particular credential lifetime, and it does not revoke an existing connection. Keep signed credentials confidential and pass them unchanged.

Errors

Invalid options and claims throw the client's CelerisException with code() ErrorCode.CONFIGURATION. The message names every field that failed and the rule it broke, such as Invalid claims. channels.references: Must not repeat a channel reference.. The message never repeats your input, secrets or claims.

Thrown byWhen
SignerOptions.Builder.build()clientId is missing, empty or contains a colon, CR or LF, or signingSecret is missing or empty, or either has an unpaired surrogate
Claims.Builder.build()The claims are invalid: missing channels, permissions or replay, or a channel reference, segment id, reference or lookback that breaks its rule
Signer.signThe clock returned a time outside 1 to 253402300799999 Unix milliseconds
Signer.credentialProviderclaimsFunction is null

An exception from your own claims function propagates unchanged from the provider's stage. Through connect(), the client reports it as TRANSPORT, like any provider failure.

We use Google cookies for analytics and for advertising measurement, only if you allow them. Until you choose, Google sets no cookies. Refer to our Cookie Policy.