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.