Skip to content

How We Use TypeBox as Our Protocol Source of Truth

I have spent way too many hours chasing bugs that were just simple type mismatches. It usually happens when you update a field on the server but forget to update the mobile app or the web client. Keeping documentation, validation logic, and multiple client libraries in sync manually is a recipe for broken production environments.

I prefer having one single source of truth. In this setup, we use TypeBox to define our Gateway WebSocket protocol. It handles everything: runtime validation, JSON Schema exports, and even Swift codegen for the macOS app. If I change the schema in one file, everything else updates automatically.

  • TypeBox: The TypeScript-first schema library used for definitions.
  • AJV: Used for runtime validation on the server and client.
  • pnpm: To run the protocol generation and check scripts.
  • Swift: If you are working with the macOS app models.

You can get a minimal client running or update the protocol in just a few steps.

Every message in the Gateway protocol follows one of these three structures:

  • Request: { type: "req", id, method, params }
  • Response: { type: "res", id, ok, payload | error }
  • Event: { type: "event", event, payload, seq?, stateVersion? }

The very first message you send must be a connect request. The server will respond with a hello-ok containing the supported methods and events.

Client Gateway
|---- req:connect -------->|
|<---- res:hello-ok --------|
|<---- event:tick ----------|
|---- req:health ---------->|
|<---- res:health ----------|

Here is how I set up a basic Node.js client to handle the handshake and a health check:

import { WebSocket } from "ws";
const ws = new WebSocket("ws://127.0.0.1:18789");
ws.on("open", () => {
ws.send(
JSON.stringify({
type: "req",
id: "c1",
method: "connect",
params: {
minProtocol: 3,
maxProtocol: 3,
client: {
id: "cli",
displayName: "example",
version: "dev",
platform: "node",
mode: "cli",
},
},
}),
);
});
ws.on("message", (data) => {
const msg = JSON.parse(String(data));
if (msg.type === "res" && msg.id === "c1" && msg.ok) {
ws.send(JSON.stringify({ type: "req", id: "h1", method: "health" }));
}
if (msg.type === "res" && msg.id === "h1") {
console.log("health:", msg.payload);
ws.close();
}
});

Whenever you modify src/gateway/protocol/schema.ts, you need to run the generation pipeline to keep the JSON and Swift models updated:

Terminal window
pnpm protocol:check

If the server closes the connection immediately, verify that your first frame is a connect request. The server also checks minProtocol and maxProtocol against its internal version; if they don’t match, the connection will be rejected.

If your macOS app is failing to decode messages, you might have forgotten to commit the generated files. Run pnpm protocol:check to verify that the committed Swift models match the current TypeBox definitions.

If you run into issues while setting up your schemas or need help with a specific method, ask the AI Setup Assistant.

OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.