Multiplayer
Add real-time multiplayer features to your Discord Activity
@robojs/sync provides real-time state synchronization for Discord Activities. It syncs state across all connected clients using WebSockets.
Setup
Install the plugin:
npx robo add @robojs/sync@nextWrap your app in SyncContextProvider. This requires @robojs/server, which is already included in scaffolded activity projects.
import { SyncContextProvider } from '@robojs/sync'
export function App() {
return (
<SyncContextProvider>
<Activity />
</SyncContextProvider>
)
}import { SyncContextProvider } from '@robojs/sync'
export function App() {
return (
<SyncContextProvider>
<Activity />
</SyncContextProvider>
)
}Shared State
useSyncState works like React's useState but syncs across all connected clients. Think of keys like channel names — components using the same key share the same state.
import { useSyncState } from '@robojs/sync'
function Counter() {
const [count, setCount] = useSyncState(0, ['counter'])
return (
<button onClick={() => setCount((prev) => prev + 1)}>
Count: {count}
</button>
)
}import { useSyncState } from '@robojs/sync'
function Counter() {
const [count, setCount] = useSyncState(0, ['counter'])
return (
<button onClick={() => setCount((prev) => prev + 1)}>
Count: {count}
</button>
)
}Two browser windows side by side showing the same activity with a synchronized counter, where clicking in one window updates both
Key Arrays
Keys determine which clients share state. Different key structures create different scopes.
| Key | Scope |
|---|---|
['counter'] | All clients share one counter |
['player', userId] | Per-user state |
['room', roomId, 'chat'] | Per-room chat |
Updating State
Use updater functions for concurrent safety:
setCount((prev) => prev + 1)setCount((prev) => prev + 1)Partial updates merge with existing state for objects. For example, updating only the x field leaves y unchanged:
const [position, setPosition] = useSyncState({ x: 0, y: 0 }, ['position'])
setPosition({ x: 100 }) // y remains 0const [position, setPosition] = useSyncState({ x: 0, y: 0 }, ['position'])
setPosition({ x: 100 }) // y remains 0Client Awareness
The third return value from useSyncState is a context object with information about connected clients.
const [state, setState, context] = useSyncState(initialState, ['room'])const [state, setState, context] = useSyncState(initialState, ['room'])| Field | Type | Description |
|---|---|---|
clients | Client[] | All connected clients |
clientId | string | Current client's ID |
isHost | boolean | Whether current client is host |
broadcast | function | Send ephemeral message to all clients |
send | function | Send ephemeral message to a specific client |
A Discord Activity showing connected users with a participant count and host indicator, demonstrating client awareness information
Colyseus
For most activities, @robojs/sync is sufficient. Consider Colyseus for complex games needing server-authoritative state — where the server is the single source of truth and validates every action, preventing cheating.
Start from a template:
npx create-robo@next my-activity --template discord-activities/react-colyseus-tsColyseus is a dedicated multiplayer game server with schema-based state, room management, and reconnection support. See the Colyseus documentation for details.
Choosing a Solution
| @robojs/sync | Colyseus | |
|---|---|---|
| Setup | One command | Manual configuration |
| Server logic | None required | Room-based handlers |
| State model | Last-write-wins (the most recent update from any client overwrites previous values) | Authoritative server (the server validates and controls all state changes) |
| Persistence | In-memory | Configurable |
| Best for | Prototypes, simple sync | Complex games, scaling |
A multiplayer activity inside Discord with multiple users interacting simultaneously, showing real-time state updates like shared cursors or a collaborative interface
