Rawtoh Rawtoh / Docs
Documentation menu

Create Your Own Module

Build a custom module to connect any tool or service to Rawtoh.

A module is any program that connects to Rawtoh via WebSocket. It can emit events (things that happen) and receive action calls (things to do). If you can write a script that opens a WebSocket, you can build a module.

How a module works

Your module connects to Rawtoh's WebSocket server and communicates using JSON-RPC 2.0. The flow is simple:

  1. Connect — open a WebSocket to the Rawtoh RPC server
  2. Register — prove you hold the module's private key by signing a server-issued challenge
  3. Subscribe — the server tells your module which events it should emit
  4. Emit events — send data to Rawtoh when something happens
  5. Receive calls — Rawtoh calls your module when an action needs something done

Step 1: Enroll your module

Modules authenticate with an Ed25519 key pair they generate themselves — never a token they're handed. Before your module can connect, bind a public key to it:

  1. Go to Settings → Module Definitions and create a new definition (or use an existing one)
  2. Go to Settings → Modules and create a new instance
  3. Copy the enrollment token — it's shown only once and expires after 15 minutes
  4. In your module, generate an Ed25519 key pair and redeem the token: POST /api/module-enroll with { token, public_key } — the response gives you the instance_id to use below

Keep the private key — it never leaves your module, and there is no way to recover it if lost; re-enroll with a fresh token instead.

Step 2: Connect and register

Open a WebSocket connection to the Rawtoh RPC server. Within 5 seconds, complete a two-step challenge/response: ask for a nonce, then sign it with your private key.

// → Send to server
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "session.challenge",
  "params": { "instance_id": "0f3c..." }
}

// ← Server responds
{ "jsonrpc": "2.0", "id": 1, "result": { "nonce": "kR3v...", "expires_in": 60 } }

// → Sign "rawtoh-module-register:v1\n<instance_id>\n<nonce>" with your Ed25519
//   private key, base64url-encode it, and send:
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "session.register",
  "params": { "instance_id": "0f3c...", "signature": "<base64url signature>" }
}

// ← Server responds
{ "jsonrpc": "2.0", "id": 2, "result": true }

If the signature is invalid, the server returns an error and closes the connection. If you don't complete registration within 5 seconds, the connection is also closed. A nonce is good for one attempt only — a failed register needs a fresh session.challenge.

Step 3: Handle subscribe calls

Right after registration, the server tells your module which events it needs. For each event, it sends a request like this:

// ← Server asks your module to subscribe to an event
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "event.subscribe",
  "params": ["chat.message"]
}

// → Your module responds with a subscription ID (any unique string)
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": "sub_abc123"
}

Rules:

Step 4: Emit events

When something happens (a chat message, a button click, a sensor reading — anything), send a notification to the server. Notifications have no id field — they're fire-and-forget.

// → Send to server (notification — no "id" field)
{
  "jsonrpc": "2.0",
  "method": "event.subscription",
  "params": {
    "subscription": "sub_abc123",
    "result": {
      "user": "alice",
      "message": "hello world"
    }
  }
}

Events are rate limited to 10 per second per connection. If you exceed this, the event is dropped.

Step 5: Expose methods (optional)

If you want actions to be able to call your module (e.g. module("my-tool").request("do.something", params)), your module needs to handle incoming JSON-RPC requests:

// ← Server sends a request to your module
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "lights.set_color",
  "params": { "color": "#ff0000" }
}

// → Your module responds
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": { "status": "ok" }
}

You define the method names and params — it's your API. Users will be able to call them from action scripts.

Required methods

Your module must handle these 3 methods from the server:

Method What you return Purpose
ping"pong"Heartbeat check
event.subscribesubscription ID or nullStart emitting an event
event.unsubscribetrueStop emitting an event

The manifest

To make your module's events and methods appear in Rawtoh's script editor (autocomplete, documentation), you can provide a manifest — an OpenRPC 1.3.2 document describing what your module can do. Set it on the module definition in Settings → Module Definitions.

OpenRPC is to JSON-RPC what OpenAPI is to REST, so the methods half is standard and works with off-the-shelf OpenRPC tooling. The events your module emits go under x-events — explained just below.

{
  "openrpc": "1.3.2",
  "info": { "title": "My Module", "version": "1.0.0" },
  "methods": [
    {
      "name": "lights.set_color",
      "summary": "Set the light color",
      "params": [
        {
          "name": "color",
          "summary": "CSS color value",
          "required": true,
          "schema": { "type": "string" }
        }
      ],
      "result": {
        "name": "result",
        "schema": {
          "type": "object",
          "properties": { "status": { "type": "string" } }
        }
      },
      "paramStructure": "by-name"
    }
  ],
  "x-events": [
    {
      "name": "sensor.temperature",
      "summary": "Temperature reading from sensor",
      "payload": {
        "type": "object",
        "properties": { "celsius": { "type": "number" } }
      }
    }
  ]
}

Three things to get right:

How you produce the document is entirely up to you: generate it from your types, from your schemas, or write it by hand. Only the document is the contract.

One caveat on schemas: Rawtoh turns them into TypeScript declarations for the script editor, and understands type, properties, required, items and enum. Anything else — $ref, oneOf, allOf, format — is accepted, but shows up as unknown in the editor. Inline your types instead of referencing components/schemas, and your users get real autocomplete.

The manifest is optional — your module will work without it. But it makes the experience much better for anyone writing automations.

Why events live under x-events

OpenRPC only describes request/response: someone calls a method, the method returns a result. That covers everything Rawtoh calls on your module, but not the other direction — your module pushing something to Rawtoh on its own initiative, which is what an event is. The spec has no object for that, so there is nothing standard to fill in.

Rather than bend methods into a shape they don't fit, events go in a specification extension. OpenRPC explicitly reserves any field starting with x- for exactly this: a compliant tool ignores what it doesn't recognise. So your document stays a valid OpenRPC document — you can still run it through an OpenRPC validator, generator or playground — and Rawtoh reads the extra part it knows about.

Because it's our extension and not the spec, we keep it plain: payload is a bare JSON Schema, with none of the Content Descriptor wrapping that result requires.

What you declare is a promise you have to keep. An x-events entry is the declaration of the protocol you already implemented in Steps 3 and 4:

That payload is what users receive as event.payload in their scripts, and your schema is the only reason they get autocomplete on it:

// In an action script, typed from your x-events payload schema
log(`It is ${event.payload.celsius}°C`);

Reconnection

Your module should reconnect automatically when the connection drops. But check the close code first:

Close code What to do
4000Don't reconnect. The user disconnected the module on purpose.
4001Don't reconnect. The instance was re-enrolled with a new key — the current private key is invalid.
Anything elseReconnect with exponential backoff: 1s → 2s → 4s → 8s → … up to 64s. Reset on successful registration.

After reconnecting, the server starts a fresh subscribe phase. Discard any old subscription IDs.

Naming convention

All event and method names use dot notation:

Summary

To build a module, your program needs to:

  1. Open a WebSocket to the Rawtoh RPC server
  2. Complete session.challenge + session.register within 5 seconds
  3. Handle ping, event.subscribe, and event.unsubscribe
  4. Emit events via event.subscription notifications
  5. Optionally handle custom method calls from actions
  6. Reconnect on disconnect (respecting close codes)

That's it. Your module can be written in any language — JavaScript, Python, Rust, Go — anything that supports WebSocket and JSON.