Server-side Usage
Sign credentials on a trusted Go server, serve them to your applications, and connect from a backend.
Before you start: add github.com/useceleris/sdk-go-server on a trusted server. Keep your Celeris client ID and signing secret in that server's configuration. Never put them in an application that you ship to users.
1. Understand the two responsibilities
sdk-go-server signs access. It does not provide a separate publish transport. A backend that publishes or receives uses sdk-go-client, just as any other application does, but it signs its own credentials through NewCredentialProvider.
Applications that you ship to users fetch credentials from an authenticated endpoint on your server instead. See Authentication.
2. Serve credentials to your applications
Create one signer from server configuration. The signer is safe for concurrent use, and it prints as redacted:
signer, err := celerisserver.NewSigner(celerisserver.SignerOptions{
ClientID: os.Getenv("CELERIS_CLIENT_ID"),
SigningSecret: os.Getenv("CELERIS_SIGNING_SECRET"),
})
if err != nil {
log.Fatal(err)
}
The endpoint authenticates the user, decides the claims server-side, and signs fresh credentials for every request:
func credentialsHandler(signer *celerisserver.Signer) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
user, ok := authenticate(request) // your session or token check
if !ok {
http.Error(writer, "unauthenticated", http.StatusUnauthorized)
return
}
var body struct {
ChannelReference string `json:"channelReference"`
}
if json.NewDecoder(http.MaxBytesReader(writer, request.Body, 4096)).Decode(&body) != nil || !user.MayAccess(body.ChannelReference) {
http.Error(writer, "forbidden", http.StatusForbidden)
return
}
credentials, err := signer.Sign(celerisserver.Claims{
Channels: celerisserver.RestrictedChannels(body.ChannelReference),
Permissions: celerisserver.RestrictedSegments(
celerisserver.SegmentClaim{SegmentID: "chat", Read: true, Write: user.Moderator},
),
Reference: user.ID, // the identity peers see; no colons
Replay: celerisserver.ReplayLookback(30 * time.Second),
})
if err != nil {
http.Error(writer, "signing failed", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(writer).Encode(credentials) // {"payload": "...", "signature": "..."}
}
}
Mount it with http.Handle("POST /api/realtime-credentials", credentialsHandler(signer)) or in any router. Serve it from an http.Server with ReadHeaderTimeout set. Never sign permissions that the caller asked for. Never cache or backdate credentials. Sign validates the claims. On failure, it returns ErrConfiguration and names the claim and the rule that failed.
Scope every credential to what the user needs. Restricted channels and segments are explicit. AllChannels() or AllSegments(...) is an opt-in, never a default. The zero Claims value is invalid. Replay and echo default to off.
3. Connect from a backend
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"os/signal"
"time"
celeris "github.com/useceleris/sdk-go-client"
celerisserver "github.com/useceleris/sdk-go-server"
)
func claimsFor(_ context.Context, request celeris.CredentialRequest) (celerisserver.Claims, error) {
if request.ChannelReference != "demo-room" {
return celerisserver.Claims{}, errors.New("worker channel is not allowed")
}
claims := celerisserver.Claims{
Channels: celerisserver.RestrictedChannels("demo-room"),
Permissions: celerisserver.RestrictedSegments(
celerisserver.SegmentClaim{SegmentID: "chat", Read: true, Write: true},
),
Reference: "demo-worker",
}
if request.Reconnect {
claims.Replay = celerisserver.ReplayLookback(min(request.ReplayLookback, 30*time.Second))
}
return claims, nil
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if err := run(ctx); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
signer, err := celerisserver.NewSigner(celerisserver.SignerOptions{
ClientID: os.Getenv("CELERIS_CLIENT_ID"),
SigningSecret: os.Getenv("CELERIS_SIGNING_SECRET"),
})
if err != nil {
return err
}
provider, err := celerisserver.NewCredentialProvider(signer, claimsFor)
if err != nil {
return err
}
client, err := celeris.NewClient(celeris.ClientOptions{CredentialProvider: provider})
if err != nil {
return err
}
channel, err := client.Channel("demo-room")
if err != nil {
return err
}
defer channel.Close()
chat, err := channel.Segment("chat")
if err != nil {
return err
}
channel.Events().OnError(func(err error) { fmt.Println("Error:", err) })
chat.OnMessage(func(payload []byte, _ celeris.MessageMetadata) { fmt.Println("Received:", string(payload)) })
if _, err := chat.Subscribe(); err != nil {
return err
}
if err := channel.Connect(ctx); err != nil {
return err
}
if err := chat.Publish(ctx, celeris.TextPayload("Hello from the backend")); err != nil {
return err
}
<-ctx.Done() // run until interrupted, e.g. Ctrl+C
return nil
}
The claims function is the authority. Nothing automatically turns the request into permission. This worker explicitly restricts its channel and segment. It replays only on reconnect, and it caps requested replay at 30 seconds. The provider calls the claims function on every connection attempt, with the attempt's context, so the function can query a database. When that context is done, the provider signs nothing. If the function returns invalid claims, Connect fails with the generic ErrTransport. To see the ErrConfiguration that names the claim and the rule, sign the function's result directly with signer.Sign.
A publish has the same local-acceptance semantics on a server as anywhere else. When Publish returns without an error, the SDK handed the bytes to the socket. That result does not confirm delivery.
4. Apply this to your service
Sign from server-owned configuration. For multi-tenant services, authorize each channel. Do not replace the restriction with 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 messages.
Next: Go server API and Go client API.