Docs

Go SDK

The full API of realtime-go, the Go WebSocket client library for backends and services.

One module covers both transports: realtime.New holds a WebSocket for live subscriptions, realtime.NewRest makes stateless HTTPS calls. Source and reference live at github.com/Foony-Limited/realtime-go.

go get github.com/Foony-Limited/realtime-go

Client constructor

import realtime "github.com/Foony-Limited/realtime-go"

client, err := realtime.New(realtime.Options{
	Key: os.Getenv("FOONY_API_KEY"),
})
Option Type Default Meaning
Key string API key, for servers and trusted contexts
Token string Static JWT, for scripts and local dev
AuthCallback func(ctx) (string, error) Returns a fresh JWT, called on connect and re-auth
Endpoint string realtime.foony.io Host or full wss:// URL
ClientID string Identity attached to key-auth connections
DisableAutoReconnect bool false Turn off the 1 s to 30 s exponential backoff
DisableQueueing bool false Fail publishes while disconnected instead of buffering
Batch *BatchOptions Auto-batching defaults for all channels

Exactly one of Key, Token, or AuthCallback is required. Go usually runs server-side, so Key is the common choice; blocking calls take a context.Context, and listeners run one at a time on a dispatcher goroutine, so a listener may call blocking SDK methods.

Connection

client.Connection.On(func(change realtime.ConnectionStateChange) {
	log.Println(change.Current, change.Reason)
})

err := client.Connect(ctx)
log.Println(client.Connection.ID(), client.Connection.ClientID())

client.Close()

States move through initialized, connecting, connected, disconnected, closing, closed, and failed. On reconnect the SDK re-attaches subscribed channels, re-enters presence it had entered, flushes queued publishes, and backfills any missed messages. Resent publishes carry stable ids, so the service deduplicates them.

Channels

channel := client.Channels.Get("chat:lobby")
client.Channels.Release("chat:lobby")

Get returns a stable instance per name and accepts realtime.WithCipher and realtime.WithBatchOptions options. Release detaches and forgets the channel. Names are 1 to 255 characters from A-Z a-z 0-9 : - _, colons for hierarchy, no leading colon.

Messages

off := channel.Subscribe(func(message *realtime.Message) {
	log.Println(message.Name, string(message.Data))
})
channel.Subscribe(listener, "typing")
channel.Subscribe(listener, "added", "removed")
channel.Unsubscribe()

err := channel.Publish(ctx, "chat", map[string]string{"text": "hello"})
err = channel.PublishBatch(ctx, []realtime.BatchMessage{{Name: "a", Data: 1}, {Name: "b", Data: 2}})
err = channel.Publish(ctx, "cursor", position, realtime.WithEphemeral())
channel.Flush()

Message.Data is a json.RawMessage: unmarshal it into your own type. Published data goes through json.Marshal. See Publish and subscribe for batching, ephemeral semantics, and the message object fields.

History

result, err := channel.History(ctx, realtime.HistoryParams{Limit: 50, Before: oldestSerial})
log.Println(len(result.Messages), result.More)

Oldest first, with Before as an exclusive serial cursor for paging backward. Details in History.

Presence

channel.Presence.Subscribe(func(event *realtime.PresenceEvent) {
	log.Println(event.Action, event.ClientID)
})
channel.Presence.On(realtime.PresenceLeave, listener)
err := channel.Presence.Enter(ctx, map[string]string{"name": "Acorn"})
err = channel.Presence.Update(ctx, map[string]any{"name": "Acorn", "busy": true})
err = channel.Presence.Leave(ctx)

Listeners receive the current members as enter events first, then live transitions. Details in Presence.

Channel lifecycle

err := channel.Attach(ctx)
err = channel.Detach(ctx)
log.Println(channel.State())

channel.On(func(change realtime.ChannelStateChange) {
	log.Println(change.Current)
})

Subscribing attaches implicitly, so Attach is only for warming a channel early or surfacing capability errors. States are initialized, attaching, attached, detaching, detached, suspended, and failed.

Token minting

Backends mint short-lived, capability-scoped JWTs for their clients locally, with no network call:

token, err := realtime.CreateJWT(os.Getenv("FOONY_API_KEY"), realtime.CreateJWTParams{
	ClientID:   userID,
	Capability: realtime.Capability{"chat:" + userID + ":*": {"subscribe", "publish"}},
	TTL:        time.Hour,
})

The key never leaves your server; hand the token to the client’s authCallback. See Auth and capabilities.

End-to-end encryption

Give a channel a symmetric key and every payload is AES-GCM encrypted before it leaves your process. The service only ever sees ciphertext, and subscribers with the same key decrypt transparently (the cipher is compatible with the JavaScript SDK’s):

key, err := realtime.GenerateRandomKey(256)
cipher, err := realtime.NewCipher(realtime.CipherParams{KeyBase64: key})
channel := client.Channels.Get("secure:room", realtime.WithCipher(cipher))
err = channel.Publish(ctx, "chat", map[string]string{"text": "only we can read this"})

Only Data is encrypted, not message names or client ids. History and REST reads decrypt the same way when given the cipher.

Rest client

rest, err := realtime.NewRest(realtime.RestOptions{Key: os.Getenv("FOONY_API_KEY")})

_, err = rest.Channels.Get("chat:lobby").Publish(ctx, "chat", map[string]string{"text": "from a job"})
history, err := rest.Channels.Get("chat:lobby").History(ctx, realtime.RestHistoryParams{Limit: 100})
members, err := rest.Channels.Get("chat:lobby").Presence.Get(ctx, realtime.RestPresenceParams{})
details, err := rest.Auth.RequestToken(ctx, realtime.TokenParams{ClientID: "user-123"})
serverTime, err := rest.Time(ctx)

Read methods return paginated results with Items, Next(ctx), HasNext(), and IsLast(). Failures return *RestError with the numeric Code and HTTP StatusCode from the error table.

Errors

Service errors are *realtime.ServerError values carrying the numeric code, exported as named constants, so handlers use errors.As instead of magic numbers:

err := channel.Publish(ctx, "chat", data)
var serverErr *realtime.ServerError
if errors.As(err, &serverErr) && serverErr.Code == realtime.CodeCapability {
	promptForUpgrade()
}