← Documentation

Game chat

Full chat message, author, history, moderation, state and connection types. The source reference lists all methods and options, including lower-level adapters; check npm release availability separately.

Connect to the current game’s default channel or a channel its backend defines. Messages travel over WebSockets. This module cannot read or send platform DMs, notifications, or another game’s chat.

Default platform chat panel

Play pages include a chat panel to the left of the game, outside its iframe. No creator integration is required. It uses the same game channel as chat.connect('game'), with the same history, moderation and block filtering. It is game chat, not platform DMs.

Chat starts open. Players can collapse or expand it with the panel icon in the game header without losing their draft or disconnecting. Enter sends a message; Shift+Enter inserts a newline. The round up-arrow inside the input also sends the message.

If your game renders its own chat UI, explicitly hide the platform panel only after that UI is ready. This browser-only control affects the current play page, not other players or the channel itself. New play sessions and iframe reloads restore creator-controlled visibility; a player’s own collapse choice is independent.

// After your custom chat UI is connected and ready:
await chat.setDefaultPanelVisible(false)

// Restore the platform panel when removing your custom UI:
await chat.setDefaultPanelVisible(true)

Hiding the panel does not clear history or disable chat. Displayed authors remain creator-supplied labels, not verified identity badges. Narrow screens preserve the left/right layout with horizontal scrolling rather than placing chat over the game.

In a game

import { chat } from '@silicon-jungle/inkwell-sdk/chat'

const channel = await chat.connect('game', {
  onMessage: message => renderMessage(message),
  onModeration: event => removeOrClearMessages(event),
  onState: state => showConnectionState(state),
})

// A game-scoped routing ID, compatible with backend connection.identity.playerId.
console.log(channel.playerId)

const messageId = crypto.randomUUID()
await channel.send('Hello!', {
  id: messageId, // reuse this ID if retrying the same send after an error
  author: { displayName: 'Your character name' },
})

// Optional in-game recipients; not a platform DM.
await channel.send('Your party is ready.', { recipients: [otherPlayerId] })

// History is loaded on connection; subscribe before connect resolves to render it.
console.log(channel.messages)
const page = await channel.history(0)
if (page.hasMore) await channel.history(page.nextCursor)
channel.close() // when leaving the scene or game

The SDK obtains fresh five-minute channel access automatically, reconnects with backoff, catches up on retained history, and suppresses duplicate deliveries. An interrupted send can have reached the server: retry using the same message ID. Idempotency lasts while that message or its removal marker remains in the bounded history.

In a backend

// Hosted backend context:
await context.chat.define('announcements', { serverWritesOnly: true })
await context.chat.define('match:123')
await context.chat.channel('announcements').send('Round starting!', {
  author: { displayName: 'Game host' },
})
await context.chat.channel('match:123').send('You found a key.', {
  recipients: [connection.identity.playerId],
  author: { displayName: 'Dungeon guide' },
})

await context.chat.channel('match:123').remove(messageId)
await context.chat.channel('match:123').clear()
await context.chat.channel('match:123').delete()

// A backend can also subscribe through the optional SDK module:
import { createRuntimeServices } from '@silicon-jungle/inkwell-sdk/storage'
const services = createRuntimeServices()
const chatConnection = await services.chat.connect('match:123', {
  onMessage: message => handleGameMessage(message),
})

Channel names are not access secrets. Named channels are joinable by players of that game. Directed messages are delivered to their recipients, the sending player, and the game backend. They are not confidential from the creator. Display names and avatars are game-supplied; Inkwell does not certify human authorship inside a game. Render message bodies as text, never raw HTML.

Connection and UI contract

import { chat, type ChatMessage } from '@silicon-jungle/inkwell-sdk/chat'

export async function openGameChat(render: (messages: readonly ChatMessage[]) => void) {
  const channel = await chat.connect('game', {
    onState: state => console.log('Chat:', state),
  })
  const refresh = () => render(channel.messages)
  const stopMessages = channel.onMessage(refresh)
  const stopModeration = channel.onModeration(refresh)
  refresh() // Includes history received before connect resolved.
  return {
    // Hide only after your own chat UI is ready; restore it when that UI closes.
    hideDefaultPanel: () => chat.setDefaultPanelVisible(false),
    showDefaultPanel: () => chat.setDefaultPanelVisible(true),
    send: (body: string, retryId: string) => channel.send(body, { id: retryId }),
    close() {
      stopMessages()
      stopModeration()
      channel.close()
    },
  }
}
// Keep retryId for one logical message; display bodies as text, never HTML.
// Call the returned close() when your chat scene is removed.

Register onMessage/onModeration/onState as connect options so initial history is not missed. The resolved connection provides playerId, current messages, send, history and close. A send resolves to its message result, not proof that another player read it. History is bounded and paginated by sequence cursor, not a permanent transcript.

onState reports connection transitions; render reconnecting or disconnected UI rather than pretending sends are confirmed. close stops renewal/reconnect and releases the channel. An identity change fails closed and clears cached private history. Subscription state is local to this connection; creating another one does not reuse your listeners.

Render text bodies and handle moderation removal, channel clear and visibility events. Do not append history blindly after reconnect: use message identity/sequence and the SDK's current messages to reconcile. Directed messages are in-game routing, not invitations, friend-only platform DMs or end-to-end encryption. A named channel alone does not implement private-room membership.

Handle rejected connect/send/history promises; errors may represent lost access, invalid input, rate limits or transport failure. An uncertain send must retain its original message ID. Do not generate fresh IDs in a retry loop or assume idempotency survives history expiration. Rate limits are shared, so opening more channels/connections does not provide more allowance.

Limits and retention

Account blocks filter live delivery and history using the authenticated connection's senderId, in either direction. Creator-supplied author names, IDs and avatars never determine this filter. Backend messages use the backend sender identity and remain creator content, including when the creator chooses to relay a player's words. Guests have no account block list; these controls do not certify human authorship.

The SDK removes newly hidden messages from channel.messages and emits chat.visibility through onModeration. Update your rendered message list when that event arrives. Access and blocks are rechecked on delivery, history requests, reconnects and periodic idle checks. Unblocking permits future delivery and authorized history reads; use channel.history(0) to reload older messages. Inkwell cannot erase content a game has already copied outside the SDK.

  • 128 channels per game, including the permanent default channel named game.
  • 1,000 retained messages/removal markers per game across all channels, for at most 24 hours. This does not change platform DM retention.
  • 2,000 characters per message; up to 32 recipients; 16 KiB maximum command size.
  • 20 sends per player per minute, shared across channels and connections. Backends can send 240 per minute.
  • 120 commands per player per minute; 600 for a backend; 2,400 across the game.
  • Four concurrent chat connections per player or backend, with 500 per game.

Guests can join game chat. Persistent personal achievements and leaderboard submissions still require sign-in. Games with more demanding channel capacity should contact the platform rather than creating unbounded channels.

HTTP API

Creators can also manage their own game’s channels through POST /api/v1/games/:slug/chat with a creator API key or signed-in creator session. This works for static games without a hosted backend. Never include a creator key in a browser build.

POST /api/v1/game-services/chat accepts a game runtime bearer credential on the backend. Browser SDK calls use the host’s session bridge. Supported operations are connect, list, history, send, define, remove, clear, and deleteChannel. Game identity comes from the credential, never a caller-supplied game ID. Channel management requires a backend credential.