Ludicord
Guide 14 · Full stack

WebSockets

Create typed realtime endpoints, events, route rooms, and Activity-instance rooms.

Ludicord 3.1.0Public source 20c4889
Audience routingBroadcast to the room that owns the event
Framework managed
CHSA
room/gamescore:updatetyped payload
YUNO

Ludicord WebSocket routes are authenticated server modules under app/ws. The client hook connects to the matching route, dispatches named events, and can reconnect with exponential backoff.

#Create a server route

app/ws/chat/route.tsts
1import { defineWS } from "ludicord/ws/server";
2
3export default defineWS({
4 connect(client) {
5 client.activity.join();
6 client.emit("ready", { clientId: client.id });
7 },
8 events: {
9 message(client, data) {
10 client.activity.broadcast("message", data, { includeSelf: true });
11 },
12 },
13 disconnect(client) {
14 client.activity.leave();
15 },
16});

defineWS() validates public event names. Names beginning with $ludicord: or _ludicord are reserved for the framework.

#Connect from React

app/embeds/chat/embed.tsxtsx
1import { useEffect, useState } from "react";
2import { useWS } from "ludicord/ws/client";
3
4export default function embed() {
5 const socket = useWS("/ws/chat");
6 const [messages, setMessages] = useState<unknown[]>([]);
7
8 useEffect(() => socket.on("message", (message) => {
9 setMessages((current) => [...current, message]);
10 }), [socket]);
11
12 return <button disabled={socket.status !== "open"} onClick={() => socket.emit("message", { text: "Hello" })}>Send</button>;
13}

on() returns an unsubscribe function, so it works directly as the cleanup returned by useEffect.

#Understand connection status

StatusMeaning
connectingThe first connection is opening
openEvents may be emitted
reconnectingA dropped connection is waiting or opening again
closedClosed manually or reconnect attempts exhausted
errorThe current socket reported an error

Calling emit() before open throws. Disable the action, queue it in application state, or use shared Activity state for optimistic synchronization.

#Choose a broadcast scope

The server client has three useful scopes:

APIAudience
client.emit()This connection only
client.broadcast()Other clients on the same route
client.room.broadcast(name, ...)Connections that joined a custom route room
client.activity.broadcast()Connections in the verified Discord Activity instance

Activity rooms require a verified instanceId. They prevent two separate launches of the same application from sharing game state accidentally.

#Read trusted connection context

client.ludicord contains the verified session. client.params contains dynamic WebSocket route params.

tsts
1const userId = client.ludicord.user.id;
2const roomId = client.params.roomId;

Params remain untrusted input. Use the session and server data to authorize joining a custom room.

#Reliability and limits

Ludicord supports heartbeat checks, payload limits, optional compression, backpressure protection, inbound events-per-second limits, and graceful shutdown. Reconnect behavior is configurable globally and per client.

ludicord.config.mjsjs
1websocket: {
2 heartbeatInterval: 30_000,
3 maxPayload: 1_048_576,
4 maxMessagesPerSecond: 120,
5 reconnect: { enabled: true, attempts: 10, initialDelay: 500, maxDelay: 10_000 }
6}

Validate every incoming event payload before storing or broadcasting it. Transport authentication does not make the payload safe.

#Hot updates

Changing a WebSocket route in development reloads the route without restarting the Ludicord server. Existing affected connections close with a service-restart signal and the client hook reconnects according to policy.

#Next step

Open Shared Activity state for a higher-level, revision-safe way to synchronize common multiplayer values.