Skip to main content

Channels

Channels are app-wide message groups for your game. Unlike spatial messages (which reach nearby players), a channel message is delivered to every member of the channel, wherever they are — ideal for guild chat, party voice coordination, trade broadcasts, or app-wide announcements.

A channel is a kind of player group (group_type channel), so it shares the same membership and role model as teams: a channel has members, roles (a leader role is created automatically for the creator), and per-role permissions. Two permissions are channel-specific: send_messages, which controls who may publish messages to the channel, and send_voice, which controls who may send audio to it. Receiving is available to every active member.

All channel management runs over the Game API GraphQL endpoint; publishing and receiving messages run over the realtime UDP path (the same udpNotifications subscription used for spatial traffic). Every operation requires an app-scoped token for this app (send Authorization: Bearer <token>; mint one with mintAppToken — see Authentication).

Deciding who can create and join channels​

Each app has a channel policy with two settings:

  • creation policy — who may create channels: admin (only app admins), member (any player with app access), or anyone.
  • default membership policy — how new channels accept members by default: open (join instantly), request (join request awaits approval), invite (managers add members), or admin (app admins only).

App admins set the policy with setChannelPolicy (requires manage_apps), and read it with channelPolicy(appId):

mutation {
setChannelPolicy(input: {
appId: "1",
creationPolicy: "member",
defaultMembershipPolicy: "open",
maxMembers: 500, # optional cap on members per channel
maxGroupsPerUser: 20 # optional cap on channels a user may create/join
}) { creationPolicy defaultMembershipPolicy maxMembers maxGroupsPerUser }
}

maxMembers and maxGroupsPerUser are optional limits — omit them (or pass null) for no cap.

Creating a channel​

mutation {
createChannel(input: {
appId: "1",
name: "Global Trade",
membershipPolicy: "open", # optional; defaults to the app policy
membersCanSend: true, # optional; defaults to true
membersCanSpeak: false # optional; defaults to false
}) { groupId name membershipPolicy }
}

The creator becomes the owner with the system leader role (which holds every channel permission, including send_messages).

membersCanSend controls the default for joiners:

  • true (open chat): a default member role granting send_messages is created and auto-assigned to everyone who joins, so all members can post.
  • false (announce / read-only): joiners receive messages but cannot post. Grant a posting role explicitly to chosen members (see Roles below).

membersCanSpeak: true adds send_voice to that default role, so every member may also send audio (a party or guild voice channel). It is off by default: only roles you grant send_voice may speak.

Grid channels​

A grid channel belongs to one grid. Its owner creates it, and the CLIENT halves of that grid's mods may post into it with emit_channel; code on any other grid may not.

mutation {
createGridChannel(input: { appId: "…", gridId: "…", name: "plot-42-chat" }) {
groupId
gridId
}
}
query {
gridChannels(appId: "…", gridId: "…") { groupId name }
}
  • Only the grid's current owner may create one, whatever the app's channel creation policy says; a grid holds at most 8 active channels.
  • membershipPolicy defaults to open, so visitors can joinChannel, and members get send_messages unless membersCanSend: false.
  • A CLIENT half posts as an actor uuid the visitor's page derives for the grid and the name the CLIENT half gives, never as the visitor: in CrowdyJS, clientHalfActorUuid(gridId, name), the first 16 bytes of SHA-256 over crowdy/client-half-actor/v1, the grid id and the name (each separated by a zero byte), as 32 hex characters. The server does not check that uuid, so treat it as a label for display, not as proof of which mod sent a message.
  • A grid-scoped token may join, leave and post only to its own grid's channels.

Joining and leaving​

mutation { joinChannel(groupId: "12") { status } } # open -> active, request -> pending
mutation { requestToJoinChannel(groupId: "12") { status } }
mutation { leaveChannel(groupId: "12") }

myChannels(appId) lists the channels you belong to, with your roles and your effective channel permissions. channels(appId) lists the active channels in an app, and channel(groupId) fetches one.

Managing members and roles​

Members whose role grants manage_members (plus app admins) can add and remove members; adding also approves a pending request. Players can always remove themselves.

mutation { addChannelMember(groupId: "12", userId: "42") { userId status } }
mutation { removeChannelMember(groupId: "12", userId: "42") }

A channel's roles carry these permission keys:

KeyAllows
send_messagesPublish messages to the channel
send_voiceSend audio to the channel
manage_groupRename/delete the channel and edit its settings
manage_membersAdd and remove members
manage_rolesCreate/edit/delete roles and assign them
invite_membersInvite players to the channel

For an announce channel, create a posting role and assign it to your broadcasters:

mutation {
createChannelRole(input: {
groupId: "12", roleName: "broadcaster", permissions: ["send_messages"]
}) { groupRoleId roleName permissions }
}

mutation {
setChannelMemberRoles(input: {
groupId: "12", userId: "42", roleIds: ["88"]
}) { userId roles { roleName } }
}

Use channelRoles(groupId) / channelMembers(groupId) to list a channel's roles and members, and updateChannelRole / deleteChannelRole to change them (the system leader role can't be deleted). Holders of manage_group (and app admins) rename or retire the channel itself with updateChannel(input) and deleteChannel(groupId).

Sending and receiving messages​

Publishing and delivery happen over the realtime UDP path. Browser clients use the sendChannelMessage mutation; native clients send the CHANNEL_MESSAGE_REQUEST UDP message directly (see the Replication API wire formats). Like other realtime sends, sendChannelMessage opens a UDP proxy session automatically for your app-scoped token (the token you minted for this app — not the identity session token login returns).

mutation {
sendChannelMessage(input: {
channelId: "12",
uuid: "0123456789abcdef0123456789abcdef", # your actor UUID (32 bytes)
payload: "aGVsbG8=", # base64, app-defined, <= 1024 bytes
sequenceNumber: 1
})
}
  • The payload is opaque to the server — encode whatever your app needs (chat text, a compact binary event, etc.).
  • The sender must be an active member holding send_messages, and its access tier for the channel's app must grant use_text_chat (every tier that grants access does unless the app took it away; see Permissions); otherwise the message is rejected (a GenericErrorResponse with UNAUTHORIZED is returned on udpNotifications).
  • The sender does not receive an echo of its own message.

Every other active member receives a ChannelMessageNotification on the shared udpNotifications subscription. As with every realtime subscription, open the WebSocket with an appId in its connection_init payload — the session is app-scoped (see the UDP Proxy API guide):

subscription {
udpNotifications {
__typename
... on ChannelMessageNotification {
channelId
uuid # the sender's actor UUID
payload # base64, exactly as sent
sequenceNumber
epochMillis
}
... on GenericErrorResponse { sequenceNumber errorCode }
}
}

Messages are ephemeral — the server routes them and does not store history. Keep your own log client-side if you need scrollback.

Limit delivery by distance​

sendRangedChannelMessage publishes to a channel but delivers only to the members near an origin chunk, such as a shout heard by party members in earshot or a squad order for the members on the same front. A member receives it when one of its live actors is in the origin's app and within maxDistance chunks of chunk, measured as the straight-line distance between chunk coordinates with the boundary included.

mutation {
sendRangedChannelMessage(input: {
channelId: "12",
uuid: "0123456789abcdef0123456789abcdef", # your actor UUID (32 bytes)
payload: "aGVsbG8=", # base64, app-defined, <= 1024 bytes
appId: "42", # the app this token is for
chunk: { x: "10", y: "0", z: "-4" }, # the origin, usually the sender's chunk
maxDistance: 5, # chunks, inclusive
sequenceNumber: 2
})
}
  • Distance is Euclidean, not the spatial rings. A member 3 chunks east and 4 north of the origin is exactly 5 away and receives it with maxDistance: 5; one 4 east and 4 north is about 5.66 away and does not, although the Chebyshev rings of spatial messages put both in ring 4. maxDistance: 0 reaches only the origin chunk. Any distance up to 2147483647 is accepted; there is no cap of 8.
  • Members receive the ordinary ChannelMessageNotification, so nothing changes on the receiving side. A member whose actors are all out of range, and a member with no live actor at all, receives nothing. The sender gets no echo.
  • The same right as sendChannelMessage: an active member holding send_messages. The origin appId must be the token's app. Refusals arrive on udpNotifications as a GenericErrorResponse with your sequenceNumber: UNAUTHORIZED without the send right, INVALID_APP_ID for another app.
  • A grid token may send it into one of its grid's channels from a chunk inside its box, as for the other grid-scoped sends (see Grid tokens).

Native clients send the CHANNEL_MESSAGE_RANGED_REQUEST UDP message (type 32; see the Replication API wire formats).

Voice on a channel​

Audio packets can go to a channel too, so a party or a guild can talk wherever its members are (replication v0.37.0). It works like a channel message, with its own right:

  • The sender must be an active member whose role grants send_voice, and hold the use_voice_chat permission for the app (the same key spatial voice needs). A missing one is refused with UNAUTHORIZED on udpNotifications.
  • Every other active member receives a ChannelAudioNotification, wherever they are; the sender gets no echo.
  • The payload is opaque, at most 1,024 bytes per packet, and carries whatever your app's audio format needs (codec, sequence, timing). The SDKs offer an optional voice header and jitter buffer you can adopt.
mutation {
sendChannelAudio(input: {
channelId: "12",
uuid: "0123456789abcdef0123456789abcdef", # your actor UUID (32 bytes)
payload: "AQEAAAAAAAAUAA==", # base64 audio packet, <= 1024 bytes
sequenceNumber: 3
})
}

subscription {
udpNotifications {
__typename
... on ChannelAudioNotification { channelId uuid audioData sequenceNumber epochMillis }
}
}

A mutation per packet suits a test, not a conversation: send sustained voice over the binary realtime relay or the UDP path, as CHANNEL_AUDIO_REQUEST (type 35; members receive CHANNEL_AUDIO_NOTIFICATION, type 36; see the Replication API wire formats).

Every member's downlink is billed. One speaker sending 50 packets a second of 100 bytes to a 20-member channel delivers about 19 × 50 × 100 bytes, roughly 95 KB a second, as egress. Keep voice channels small, or use spatial voice (sendAudioPacket) for proximity chat.

Reference​

See the Game API GraphQL reference for the exact inputs and return types, and Permissions overview for how channel roles fit with app tiers and grid permissions.