Channels
The SDK exposes app-wide message channels via client.channels (CRUD and
membership) and client.udp (publishing and receiving messages). A channel
message reaches every member of the channel regardless of where they are in the
world. See the Game API Channels guide for the full
permission model.
Manage channels
Channels are a Game API surface, so they need an app-scoped token: log in on an identity client, mint a token for the app, and drive channels from a per-game client (see Portals & app-scoped tokens).
import {
BrowserLocalStorageTokenStore,
createCrowdyClient,
} from '@crowdedkingdoms/crowdyjs';
const identity = createCrowdyClient({
httpUrl: 'https://ck.prod.crowdedkingdoms.com/graphql',
tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:session'),
});
await identity.auth.login({ email: 'player@example.com', password }); // see /crowdyjs/readme#sign-in-with-clientauth
const appToken = await identity.portal.mintAppToken('1');
const game = createCrowdyClient({
httpUrl: appToken.gameApiUrl ?? 'https://ck.prod.crowdedkingdoms.com/graphql',
wsUrl: appToken.gameApiWsUrl ?? 'wss://ck.prod.crowdedkingdoms.com/graphql',
tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:app:1'),
});
game.setToken(appToken.token);
// Create an open chat channel (members can post by default).
const channel = await game.channels.create({
appId: '1',
name: 'Global Trade',
membershipPolicy: 'open',
membersCanSend: true,
membersCanSpeak: false, // true: every member may also send channel audio
});
await game.channels.join(channel.groupId);
const mine = await game.channels.mine('1'); // channels I belong to
const members = await game.channels.members(channel.groupId);
Other methods mirror the GraphQL surface: list, get, roles, policy,
update, remove, requestToJoin, leave, addMember, removeMember,
ban, unban, setMemberRoles, createRole, updateRole, deleteRole, and
setPolicy.
Ban a member
A ban keeps someone out of the channel until it is lifted: they are removed, the channel's
messages stop reaching them and theirs stop reaching it, and joining, requesting to join or
being added fails with GROUP_MEMBER_BANNED. Leaving or being removed does not lift it. It
takes the channel's manage_members permission (CrowdyJS 18.10.0, and the Game API release
after v2.43.1).
import { groupMemberBannedOf } from '@crowdedkingdoms/crowdyjs';
await game.channels.ban(channel.groupId, '2048', 'spamming trade chat');
await game.channels.unban(channel.groupId, '2048');
try {
await game.channels.join(channel.groupId);
} catch (err) {
if (groupMemberBannedOf(err)) showMessage('You are banned from this channel.');
}
A player whose access to the app is revoked or suspended loses every channel at once; see revoking, suspending and banning.
Send and receive messages
Subscribe with a channelMessage handler — passing the appId as the second
argument, since every realtime subscription is app-scoped — then publish with
client.udp.sendChannelMessage. The first subscriber opens the shared realtime
WebSocket; publishing opens the UDP proxy session automatically.
const unsubscribe = game.udp.subscribe(
{
channelMessage: (msg) => {
// msg.channelId, msg.uuid (sender), msg.payload (base64), msg.sequenceNumber
const text = Buffer.from(msg.payload, 'base64').toString();
console.log(`[${msg.channelId}] ${text}`);
},
genericError: (err) => console.warn('channel error', err.errorCode),
},
'1', // the app whose realtime session this subscription opens
);
await game.udp.sendChannelMessage({
channelId: channel.groupId,
uuid: myActorUuid, // 32-byte UTF-8 actor id
payload: Buffer.from('hello').toString('base64'),
sequenceNumber: 1,
});
// later
unsubscribe();
Notes:
- The sender must hold the channel
send_messagespermission; otherwise the publish is rejected (delivered as agenericErrorwithUNAUTHORIZED). - The sender receives no echo of its own message.
- Payloads are opaque (base64) and messages are ephemeral — keep your own scrollback client-side if needed.
Voice on a channel
CrowdyJS 18.7.0 sends and receives channel audio (replication v0.37.0): a party or guild
can talk wherever its members are. The sender needs the channel's send_voice right
(membersCanSpeak: true on channels.create gives it to every member) and use_voice_chat;
a refusal arrives as a genericError with UNAUTHORIZED.
game.udp.subscribe(
{
channelAudio: (a) => jitter.push(`${a.channelId}:${a.uuid}`, fromBase64(a.audioData)),
},
'1',
);
const packetizer = new VoicePacketizer({ codec: VoiceCodec.OPUS, frameMs: 20 });
await game.udp.sendChannelAudio({
channelId: channel.groupId,
uuid: myActorUuid,
payload: toBase64(packetizer.packetize(opusFrame)),
});
sendChannelAudiosends opcode 35 on the binary relay whenrealtime.binaryTransportis on, and falls back to the GraphQL mutation; members receive it through the samechannelAudiohandler either way. Prefer the relay for sustained voice.- The payload is opaque, at most 1,024 bytes; the voice helpers are an optional format. The sender gets no echo.
- Every member's downlink is billed as egress, so keep voice channels small.
Limit delivery by distance
client.udp.sendRangedChannelMessage (CrowdyJS 18.5.0) publishes to a channel but
delivers only to the members with a live actor within maxDistance chunks of an
origin chunk, by straight-line distance with the boundary included. Members receive
it through the same channelMessage handler, so the receiving code above does not
change.
await game.udp.sendRangedChannelMessage({
channelId: channel.groupId,
uuid: myActorUuid,
payload: Buffer.from('over here').toString('base64'),
appId: '1', // the app this game client's token is for
chunk: { x: '10', y: '0', z: '-4' }, // the origin, usually your actor's chunk
maxDistance: 5, // chunks, inclusive
sequenceNumber: 2,
});
A member 3 chunks east and 4 north of the origin (exactly 5) receives it; one 4 east
and 4 north (about 5.66) does not. maxDistance is 0 (the origin chunk only) to
CHANNEL_RANGED_MAX_DISTANCE (2147483647); a larger one, or a payload over 1024 bytes,
is refused with a CrowdyGraphQLError and reaches nobody. It takes the same right as sendChannelMessage, the
sender receives no echo, and a member with no live actor receives nothing. On the
binary relay it is sent as the CHANNEL_MESSAGE_RANGED_REQUEST datagram
(serializeRangedChannelMessage); see the
Game API channels guide.