Skip to content

Backend: Go

moqom-go is the server-side SDK for Go. It is for your backend, not for participants: it mints the tokens your users join with, creates and closes rooms, moderates them, and follows what happens in them. It does not carry media and has no QUIC dependency.

Module github.com/moqom-cloud/moqom-go
Go 1.23 or later
Talks to The control-plane gRPC API at api.moqom.cloud:443
Licence Apache-2.0
Terminal window
go get github.com/moqom-cloud/moqom-go

Authenticate with a backend API key (mqk_…) from moqom.cloud or POST /v1/keys. Keep it in your secret manager.

client, err := moqom.Dial("api.moqom.cloud:443",
moqom.WithCredential(os.Getenv("MOQOM_API_KEY")),
moqom.WithTenant(os.Getenv("MOQOM_TENANT")))
if err != nil {
return err
}
defer client.Close()

Other options: WithActor (a default actor for every call), WithRetry(attempts, base), WithTLS, WithDialOptions, and WithConnection for tests.

The call you will make most. Authenticate your user however you already do, then ask for a token naming that user, that device, that room, and exactly the rights they should have.

token, err := client.MintToken(ctx, moqom.TokenRequest{
Username: user.Name,
DeviceThumbprint: req.Thumbprint, // from the client SDK's DeviceKey
Room: "live/42",
Publish: moqom.PublishAudio | moqom.PublishVideo,
Preset: moqom.PresetNone,
Lifetime: 10 * time.Minute, // default 10 min, max 15 min
})
// token.Token, token.ExpiresAt, token.Identity, token.KeyID
Field Meaning
Username Your identifier for the user. Normalised (NFC); read back the minted one from token.Identity.
DeviceThumbprint The client device key’s thumbprint, reported by the client SDK. Required.
Room Which room. Empty mints a token for the tenant’s discovery surface but no room.
Publish PublishAudio, PublishVideo, PublishScreenShare, PublishData; PublishCamera = audio+video.
Moderation / Preset Capabilities, or a preset: PresetNone, PresetModerator, PresetSuperModerator, PresetOwner.
Owner Make this user the room’s owner.
Backstage Join backstage; going live later needs no new token.
AgeVerified Assert you verified the user’s age, for rooms with an age gate.
Lifetime Zero for the default (10 minutes). Maximum 15 minutes.

MintToken is not retried automatically on an ambiguous failure — minting is cheap and a second token is harmless. Ask again.

room, err := client.CreateRoom(ctx, moqom.NewRoom{
Name: "live/42",
Owner: "alice",
Policy: moqom.Policy{MaxParticipants: 500, StageSlots: 8},
})
r := client.Room("live/42")
state, _ := r.Get(ctx)
page, _ := r.Participants(ctx, moqom.ParticipantQuery{})
_, _ = r.UpdatePolicy(ctx, moqom.PolicyUpdate{ /* fields to change */ })
_, _ = r.Close(ctx, moqom.CodeOperatorAction, moqom.Actor{OnBehalfOf: "jane"})

CreateRoom is idempotent on the name: creating a room that exists returns ErrExists rather than resetting it. client.Rooms pages through rooms; client.AllRooms collects every page.

Watchdogs: r.AttachWatchdog, r.DetachWatchdog, r.Watchdogs.

Attribute every action to the human who asked for it. The service account comes from your credentials and cannot be set per call, so the audit trail cannot be forged.

r := client.Room("live/42")
entry, err := r.Kick(ctx, moqom.KickRequest{
Target: moqom.User("carol"),
Code: moqom.CodeHarassment,
Actor: moqom.Actor{OnBehalfOf: "jane", Reason: "repeated slurs in chat"},
})
_, err = r.Ban(ctx, moqom.BanRequest{
Subject: moqom.BanAccount("carol"),
Duration: 24 * time.Hour, // zero = permanent
Code: moqom.CodeHarassment,
Actor: moqom.Actor{OnBehalfOf: "jane"},
})
_, err = r.ForceMute(ctx, moqom.MuteRequest{
Target: moqom.User("dan"),
Media: []moqom.MediaKind{moqom.MediaAudio},
Scope: moqom.MuteForAudience, // cohosts still hear him
Actor: moqom.Actor{OnBehalfOf: "jane"},
})
_, err = client.BanFromTenant(ctx, moqom.BanRequest{Subject: moqom.BanDevice(identity)})

Every call returns the ModerationEntry it produced. Check entry.Enforced: true means live sessions have been acted on; false means the record exists and enforcement is still being completed (MOQOM keeps retrying). Show that to your moderator.

See the Moderation guide for the full set of verbs and their semantics.

Events is your substitute for being in the room. Delivery is at-least-once and ordered per room, and each event’s Sequence is contiguous within its room — persist it.

stream, err := client.Room("live/42").Events(ctx, moqom.EventQuery{After: cursor})
if err != nil {
return err
}
for {
event, err := stream.Recv()
if err != nil {
return err // reconnect from your cursor, not from the present
}
switch event.Kind {
case moqom.EventParticipantJoined:
log.Println("joined", event.Joined.Participant.Identity.Username)
case moqom.EventModerationApplied:
audit(event.Moderation)
}
cursor = event.Sequence
}

client.Events(ctx, query) follows every room in the tenant — the right choice for a moderation queue. stream.All() returns an iter.Seq2 for range-over-func.

Compare with errors.Is; you never need to import gRPC:

Sentinel Meaning
ErrNotFound No such room, participant or watchdog (or it is in another tenant).
ErrDenied Permission: the key’s ceiling, the actor’s capabilities, or standing over the target.
ErrUnauthenticated Missing, malformed or unknown API key.
ErrInvalid Malformed request. Retrying will not help.
ErrExists The room already exists.
ErrClosed The room has ended.
ErrUnavailable Transient failure that outlasted the SDK’s retries. Retry later with the same idempotency key.
ErrInternal MOQOM’s bug.

*moqom.Error carries Op (e.g. "Room.Kick"), the gRPC Code, the server’s Message, and Retryable().

Every mutating call carries an idempotency key, generated by the SDK when you do not supply one, and retries reuse it. A queue that redelivers “ban for 24 hours” does not extend the ban. When a retry might come from a different process, set your own key on the Actor.

There are no numeric user IDs. A username is a string you already have; a device is derived from the client’s device-key thumbprint — so a device ban and the thumbprint you passed to MintToken are the same fact.