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.
Define one protocol
Section titled “Define one protocol”Keep payload types in src/shared/ so both entries import the same contract:
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.
Connect both directions
Section titled “Connect both directions”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:
import { defineNetwork } from "@killscript/sdk/client";import type { ClientToServer, ServerToClient } from "../shared/messages";
export const coreNetwork = defineNetwork<ClientToServer, ServerToClient>("core");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.
Send an intent and return the result
Section titled “Send an intent and return the result”// client featurelet confirmedEnabled = false;
coreNetwork.on("stateChanged", ({ enabled }) => { confirmedEnabled = enabled; updateStatusLabel(enabled);});
controls.toggle.onPressed(() => { coreNetwork.send("setEnabled", { enabled: !confirmedEnabled, });});// server featurelet 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.
Why sends are batched
Section titled “Why sends are batched”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.
Payload rules
Section titled “Payload rules”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.