Documentation

Server-side Usage

Sign credentials on a trusted Python server, serve them to your applications, and connect from a backend.

Before you start: install useceleris-server on a trusted server, and keep your Celeris client ID and signing secret in that server's configuration, never in an application you ship to users.

1. Understand the two responsibilities

useceleris-server signs access. It has no separate publishing transport: a backend that publishes or receives uses useceleris-client, as any other application does, but signs its own credentials through create_credential_provider().

Backend signer -> backend client -> Celeris -> subscribers

Applications you ship to users fetch credentials from an authenticated endpoint on your server instead; see Authentication.

2. Serve credentials to your applications

The endpoint authenticates the user, decides the claims server-side, and signs fresh for every request:

import os

from useceleris_server import SigningClaims, create_signer

signer = create_signer(
    client_id=os.environ["CELERIS_CLIENT_ID"],
    signing_secret=os.environ["CELERIS_SIGNING_SECRET"],
)


def credentials_for(user_id: str, channel_reference: str, may_write: bool) -> dict[str, str]:
    # Your application decided this user may access this channel.
    claims: SigningClaims = {
        "channels": {"kind": "restricted", "references": [channel_reference]},
        "permissions": {
            "kind": "restricted",
            "segments": [{"segment_id": "chat", "read": True, "write": may_write}],
        },
        "reference": user_id,  # the identity peers see; no colons
        "replay": {"lookback_ms": 30_000},
    }
    credentials = signer.sign(claims)

    return {"payload": credentials.payload, "signature": credentials.signature}

Call it from your framework's authenticated route (Django, Flask, FastAPI or anything else) after deciding whether the user may access the requested channel. Never sign permissions the caller asked for, and never cache or backdate credentials. sign() is synchronous and validates the claims, raising ConfigurationError naming the claim and rule that failed.

Scope every credential to what the user needs: restricted channels and segments are explicit, and "kind": "all" is an opt-in, never a default. Replay and echo default to off.

3. Connect from a backend

import asyncio
import os

from useceleris_client import ChannelError, MessageMetadata, ServerError, create_client, read_text, text_payload

from useceleris_server import CredentialRequest, SigningClaims, create_credential_provider, create_signer

signer = create_signer(
    client_id=os.environ["CELERIS_CLIENT_ID"],
    signing_secret=os.environ["CELERIS_SIGNING_SECRET"],
)


def claims_for(request: CredentialRequest) -> SigningClaims:
    if request.channel_reference != "demo-room":
        raise ValueError("Worker channel is not allowed.")

    return {
        "channels": {"kind": "restricted", "references": ["demo-room"]},
        "permissions": {
            "kind": "restricted",
            "segments": [{"segment_id": "chat", "read": True, "write": True}],
        },
        "reference": "demo-worker",
        "replay": (
            False
            if request.replay_lookback_ms is None
            else {"lookback_ms": min(request.replay_lookback_ms, 30_000)}
        ),
    }


def report(error: ChannelError) -> None:
    print(error.type if isinstance(error, ServerError) else error.code, error)


def receive(payload: bytes, metadata: MessageMetadata) -> None:
    print("Received:", read_text(payload))


async def main() -> None:
    client = create_client(
        credential_provider=create_credential_provider(signer=signer, claims=claims_for)
    )
    channel = client.channel("demo-room")
    chat = channel.segment("chat")
    channel.events().on_error(report)
    chat.on_message(receive)
    chat.subscribe()

    try:
        await channel.connect()
        await chat.publish(text_payload("Hello from the backend"))
        await asyncio.Event().wait()  # run until cancelled, e.g. Ctrl+C
    finally:
        await channel.close()


asyncio.run(main())

The claims function is the authority: nothing from the request turns into permission by itself. If it returns invalid claims, connect() fails with the client's generic Transport error; to see which claim and rule failed, sign its result directly with signer.sign(claims_for(request)), which raises a ConfigurationError naming them. This worker restricts its channel and segment and caps the requested replay at 30 seconds. It may also be an async def, for claims that need a database lookup. The provider calls it on every connection attempt and signs freshly; cancelling the attempt while it runs cancels it, and nothing is signed.

Publishing has the same local-acceptance semantics on a server as anywhere else: when publish() returns, the bytes were handed to the socket, not confirmed delivered.

4. Apply this to your service

Sign from server-owned configuration. For multi-tenant services, authorize each channel instead of widening to unrestricted claims. The signer holds no resources and needs no disposal; the client channel owns the socket. Use Reconnection and Recovery to decide how your service reconciles missed or duplicate updates.

Next: Python server API and Python client API.

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