Documentation

Go Server API

The full reference for sdk-go-server covers NewSigner, the claims surface, and the connection to Celeris from a Go backend.

go get github.com/useceleris/sdk-go-server

The module requires Go 1.27 or newer, and it runs on trusted servers only. The package name is celerisserver. This module holds your signing secret and must never ship in a browser, mobile or desktop application. It depends on sdk-go-client for the credential types and errors, and it signs with the standard library.

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.

NewSigner()

func NewSigner(options SignerOptions) (*Signer, error)

type SignerOptions struct {
    ClientID      string // no colon, CR or LF
    SigningSecret string
    Clock         func() time.Time // nil means time.Now; a test seam
}

func (signer *Signer) Sign(claims Claims) (celeris.Credentials, error)

Sign is deterministic for fixed inputs. It validates the claims, stamps the current time and returns celeris.Credentials. A Signer is safe for concurrent use and holds no resources. It keeps the secret only inside the digest it computes, so the secret never appears when you print the signer. SignerOptions implement String, Format and LogValue, so they print as redacted through fmt and slog. fmt prints a struct's unexported fields field by field, so pass the options to NewSigner and do not keep them in a struct of your own.

signer, err := celerisserver.NewSigner(celerisserver.SignerOptions{
    ClientID:      os.Getenv("CELERIS_CLIENT_ID"),
    SigningSecret: os.Getenv("CELERIS_SIGNING_SECRET"),
})

if err != nil {
    log.Fatal(err)
}

credentials, err := signer.Sign(celerisserver.Claims{
    Channels: celerisserver.RestrictedChannels("room-42"),
    Permissions: celerisserver.RestrictedSegments(
        celerisserver.SegmentClaim{SegmentID: "chat", Read: true, Write: true},
    ),
    Reference: "user-8317",
    Replay:    celerisserver.ReplayLookback(30 * time.Second),
})

if err != nil {
    log.Fatal(err)
}

Return credentials to the application unchanged. It encodes as {"payload": ..., "signature": ...}. Both values are opaque: never parse, re-encode or cache them.

The claims surface

type Claims struct {
    Channels    ChannelScope
    Permissions SegmentPermissions
    Reference   string
    Replay      Replay
    AllowEcho   bool
}

type SegmentClaim struct {
    SegmentID string
    Read      bool
    Write     bool
}

func AllChannels() ChannelScope
func RestrictedChannels(references ...string) ChannelScope
func AllSegments(read, write bool) SegmentPermissions
func RestrictedSegments(segments ...SegmentClaim) SegmentPermissions
func ReplayBacklog() Replay
func ReplayLookback(lookback time.Duration) Replay
FieldValuesNotes
ChannelsAllChannels() or RestrictedChannels(references...)Required. References are 1 to 255 characters of [A-Za-z0-9_-], unique, at least one. None is refused, never read as "all".
PermissionsAllSegments(read, write) or RestrictedSegments(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. No colon, CR or LF. Leave it empty to let the server generate one.
ReplayReplayBacklog() or ReplayLookback(d)The zero value replays nothing. A lookback is whole milliseconds, 0 to 4294967295 ms.
AllowEchoboolDefaults to false: a connection does not receive its own publishes.

ChannelScope, SegmentPermissions and Replay are opaque: make them only with these functions. The zero Claims is invalid, and every string must be valid UTF-8. Unrestricted access is always an explicit opt-in:

credentials, err := signer.Sign(celerisserver.Claims{
    Channels:    celerisserver.AllChannels(),
    Permissions: celerisserver.AllSegments(true, false),
})
A token with Write: true but Read: false 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 receives messages, check Read first.

NewCredentialProvider()

Sometimes a backend is itself a realtime client, for example a worker that publishes events. This function bridges the signer to the client's provider.

func NewCredentialProvider(signer *Signer, claims ClaimsFunc) (celeris.CredentialProvider, error)

type ClaimsFunc func(ctx context.Context, request celeris.CredentialRequest) (Claims, error)
ArgumentRequiredMeaning
signerYesThe signer from NewSigner
claimsYesServer-owned authorization function, called with each attempt's context

The provider calls claims(ctx, request) on every attempt, then signs with a fresh timestamp. If the context is already done before claims runs, or after it returns, the provider fails and does not sign. See the client types for CredentialRequest's fields.

provider, err := celerisserver.NewCredentialProvider(signer, func(_ context.Context, request celeris.CredentialRequest) (celerisserver.Claims, error) {
    claims := celerisserver.Claims{
        Channels: celerisserver.RestrictedChannels(request.ChannelReference),
        Permissions: celerisserver.RestrictedSegments(
            celerisserver.SegmentClaim{SegmentID: "chat", Read: true, Write: true},
        ),
        Reference: "worker-1",
    }

    if request.Reconnect {
        claims.Replay = celerisserver.ReplayLookback(min(request.ReplayLookback, 30*time.Second))
    }

    return claims, nil
})

if err != nil {
    log.Fatal(err)
}

client, err := celeris.NewClient(celeris.ClientOptions{CredentialProvider: provider})

if err != nil {
    log.Fatal(err)
}

channel, err := client.Channel("room-42")

if err != nil {
    log.Fatal(err)
}

defer channel.Close()

if err := channel.Connect(ctx); err != nil {
    log.Fatal(err)
}

Your claims function runs freshly 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.

Invalid claims from the provider reach the client as its generic ErrTransport, which never repeats what went wrong. To check a claims function, sign its result directly with signer.Sign, which returns an ErrConfiguration 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 return a *celeris.Error with Code celeris.ErrConfiguration, so errors.Is(err, celeris.ErrConfiguration) matches them. The message names each 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.

Returned byWhen
NewSignerClientID is empty or contains a colon, CR or LF, SigningSecret is empty, or either value is invalid UTF-8
Signer.SignThe claims are invalid, or Clock returned a time outside 1 to 253402300799999 Unix milliseconds
NewCredentialProvidersigner or claims is nil

An error returned by your own claims function propagates unchanged from the provider. Through Connect it appears as ErrTransport, 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.