Skip to content

Typed networking

A Reflex module has two separate runtimes. The client cannot call a server function directly, and Reflex cannot update client UI directly. A network channel carries small data messages between them. TypeScript verifies each message name and its declared payload shape; transport rules still matter at the boundary.

Use it to report an intent from the client, let Reflex make the gameplay decision, and then report the resulting state back to the client.

Keep payload types in src/shared/ so both entries import the same contract:

src/shared/messages.ts
export type ClientToServer = {
setEnabled: { enabled: boolean };
selectTarget: { agentId: number | undefined };
};
export type ServerToClient = {
stateChanged: { enabled: boolean; changedAtTick: number };
rejected: { reason: string };
};

Keys are message names. Values are their payloads. A wrong name, a missing field or a field with the wrong type is a compile-time error.

The first generic is always what the current file sends; the second is what it receives. This is why the two server generics are reversed:

src/client/network.ts
import { defineNetwork } from "@killscript/sdk/client";
import type { ClientToServer, ServerToClient } from "../shared/messages";
export const coreNetwork =
defineNetwork<ClientToServer, ServerToClient>("core");
src/server/network.ts
import { defineNetwork } from "@killscript/sdk/server";
import type { ClientToServer, ServerToClient } from "../shared/messages";
export const coreNetwork =
defineNetwork<ServerToClient, ClientToServer>("core");

defineNetwork() must initialize a top-level const. Use the same namespace on both sides. The namespace allows several independent protocols to reuse a message name such as stateChanged without colliding.

The message kind in every send() and on() call must be a string literal at that call site. Branch before the call when dispatch is dynamic; do not store a kind in a variable and pass the variable to the channel.

// client feature
let confirmedEnabled = false;
coreNetwork.on("stateChanged", ({ enabled }) => {
confirmedEnabled = enabled;
updateStatusLabel(enabled);
});
controls.toggle.onPressed(() => {
coreNetwork.send("setEnabled", {
enabled: !confirmedEnabled,
});
});
// server feature
let enabled = false;
coreNetwork.on("setEnabled", (message) => {
enabled = message.enabled;
applyServerState(enabled);
coreNetwork.send("stateChanged", {
enabled,
changedAtTick: Time.Tick,
});
});

The client asks; Reflex decides and applies; the client renders the confirmed result. Avoid a client round trip for a decision Reflex can make from server state alone.

The native transport was verified to keep only one of several Network.SendTable calls made during the same simulation tick. The SDK queues all send() calls for that tick and emits one native table containing an ordered batch. The receiving side expands the batch and runs every matching handler in the original order.

coreNetwork.send("stateChanged", firstState);
coreNetwork.send("stateChanged", secondState);
coreNetwork.send("stateChanged", finalState);
// The receiver observes all three, in this order.

Several defineNetwork() declarations share one native receiver and one outgoing batch. You do not need to build another queue around them.

Payloads may contain strings, numbers, booleans, nested plain objects and dense arrays made from those values. An undefined object field is omitted by native serialization; undefined is rejected as an array element. The SDK type guard also rejects functions and method-bearing native objects such as Agent and Vector3.

Do not send any native userdata, including data-only structs that may look like plain TypeScript objects. TypeScript is structurally typed and cannot identify every such wrapper. Send stable primitive fields such as an entity ID or vector components instead.

on() registers a module-lifetime handler and intentionally returns no subscription. Put conditional behavior inside the callback or route the message to the currently active feature state.

See Client and Reflex for deciding which side should own a behavior.