Skip to main content

Adding a new Node Type

A node is one activity inside a workshop — a slide, a quiz, a whiteboard. Each one exists twice: as a class in the Nakama runtime that owns the state, and as a React component that renders it. This page walks through both halves.

tip

For troubleshooting go to "help".

The lifecycle​

workshop.ts drives the match loop and calls into the current node. DefaultNode defines five hooks; override the ones you need and ignore the rest.

HookCalled when
initthe node is entered, including when the host navigates backwards (goBack)
loopevery tick — 5× per second
messagesa client sends data for this node
matchSignalan external signal arrives (LLM result, agent update)
sendDatanode state should be broadcast to clients
Override with = function, never with method syntax

DefaultNode assigns these as instance properties, not prototype methods:

init = function (this: DefaultNode, _workshop, _state, _goBack) {};

A subclass that declares init(...) { } in normal method syntax is silently ignored — the property assigned in the constructor shadows the prototype method, so your code never runs and nothing warns you. Every existing node (QuizNode, ChatNode, ScoreboardNode, GuessItNode, …) uses = function.

The leading this: DefaultNode is a TypeScript this parameter, not an argument. It gives you typed access to this.data, this.autoNext and friends; callers do not pass it.

The hooks mutate state and this.data in place. They return nothing — do not return state.

Backend (Nakama)​

1. Extend the NodeType enum​

apps/nakama/src/models/workshop_models.ts

export enum NodeType {
Slide = 'Slide',
Quiz = 'Quiz',
Whiteboard = 'Whiteboard',
YourNewNode = 'YourNewNode', // add yours
}

The frontend has its own copy of this enum in apps/web/src/models/workshop.ts. Both must agree — the string value is the contract.

2. Implement the node​

apps/nakama/src/workshop/nodes/YourNewNode.ts

class YourNewNode extends DefaultNode {
constructor(
node: WorkshopNode_Default,
state: workshop_State,
ctx: nkruntime.Context
) {
super(node, state, ctx);
}

init = function (
this: DefaultNode,
workshop: workshopObject,
state: workshop_State,
goBack: boolean
) {
let nodeState = this.data as YourNewNodeState;

// Set up node state. `goBack` is true when the host navigated backwards,
// which usually means restoring a later step instead of starting over.
nodeState.index = goBack ? 1 : 0;
};

messages = function (
this: DefaultNode,
workshop: workshopObject,
state: workshop_State,
message: nkruntime.MatchMessage
) {
// Read message.data, update this.data, then broadcast:
this.sendData(workshop, state);
};
}

Keep the runtime constraint in mind: this code compiles to ES5. No for...of over non-arrays, no Map/Set iteration, no generators, no Symbol. Use Object.keys() and plain array methods.

3. Register it in the factory​

apps/nakama/src/workshop/workshop_Info.ts

case NodeType.YourNewNode:
tempNodeClass = new YourNewNode(tempNode, state, ctx);
break;

Without this the node falls back to DefaultNode and simply does nothing.

Frontend (Next.js)​

1. Extend the enum​

apps/web/src/models/workshop.ts — same string value as the backend.

2. Create the component​

apps/web/src/components/nodes/YourNewNode.tsx

Components receive the workshop payload and the node payload as props, and send data back through sendDataToNode:

export default function YourNewNode(props: {
workshop: WorkshopPayload;
node: NodePayload;
sendDataToNode: (opcode: number, data: object) => void;
setNavButtons: (buttons: NavButtons) => void;
}) {
const nodeData = props.node.nodeData as YourNewNodeData;

return props.workshop.isHost ? (
<div>{/* host view */}</div>
) : (
<div>{/* participant view */}</div>
);
}

Look at an existing node of similar shape before writing a new one — ImgQuizNode for host/participant splits, ChatNode for streaming updates.

3. Map it in the workshop page​

apps/web/src/app/workshop/[id]/page.tsx

case NodeType.YourNewNode:
return <YourNewNode key={index} {...nodeProps} />;

Opcodes​

If your node needs new client↔server messages, add them to the OpCode enum — which exists twice, in apps/nakama/src/models/workshop_models.ts and mirrored in apps/web/src/models/. Numbers must match; a mismatch shows up as messages silently going nowhere.

Testing​

Add a workshop JSON under apps/web/workshops/ that contains your node, then:

pnpm run dev

Open http://localhost:3000/StoragePush to push the workshop into Nakama storage, create a session and step into your node.

For a preview deployment the same applies, except the accounts are already seeded — see Deployment.