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.
What You’ll Need
Section titled “What You’ll Need”- 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.
Quick Start
Section titled “Quick Start”You can get a minimal client running or update the protocol in just a few steps.
1. Understand the Frames
Section titled “1. Understand the Frames”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? }
2. The Handshake
Section titled “2. The Handshake”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 ----------|3. Connect a Minimal Client
Section titled “3. Connect a Minimal Client”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(); }});4. Sync the Schemas
Section titled “4. Sync the Schemas”Whenever you modify src/gateway/protocol/schema.ts, you need to run the generation pipeline to keep the JSON and Swift models updated:
pnpm protocol:checkTroubleshooting
Section titled “Troubleshooting”Handshake Rejection
Section titled “Handshake Rejection”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.
Outdated Swift Models
Section titled “Outdated Swift Models”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.
What’s Next
Section titled “What’s Next”If you run into issues while setting up your schemas or need help with a specific method, ask the AI Setup Assistant.
OpenClaw Expert
Still stuck?
If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.