Skip to main content

5-Minute Quickstart — Pub/Sub over the Wire Protocol

Two clients exchanging messages over WebSocket + JWT, no SDK, no WebRTC. For chat, IoT telemetry, AI agent buses, non-JavaScript stacks — anything that needs the messaging layer without the WebRTC layer.

Using JavaScript / TypeScript?

You can still use the SDK for pub/sub — SignallingClient is the pub/sub-only class. See JS SDK Getting Started. The wire-protocol path below is for non-JS stacks (Go, Python, Java, Swift, Kotlin, Rust, Unity, etc.) or anyone who wants to see what's happening on the wire.

1. Get a secret key

Go to dashboard.metered.ca → Realtime Messaging → Keys → Create key, pick type Secret. Copy both:

  • Key ID (sk_id_…) — public identifier; goes in the JWT kid header
  • Signing secret (sk_secret_…) — shown ONCE; keep it on your server

2. Mint a JWT server-side

Your backend signs an HS256 JWT carrying the user's identity + channel scope.

mint-token.js
const jwt = require("jsonwebtoken");

const KEY_ID = "sk_id_…"; // from dashboard
const SECRET = "sk_secret_…"; // shown once at create

const token = jwt.sign(
{
sub: "alice", // becomes the peerId
channels: ["room-1"], // channels this peer may use
permissions: ["publish", "subscribe", "presence", "send"],
exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour
},
SECRET,
{ algorithm: "HS256", header: { alg: "HS256", kid: KEY_ID } },
);

console.log(token);

Don't have a JWT library for your stack? Call the REST mint endpoint instead: POST https://rms.metered.ca/v1/tokens with your key pair as Authorization: Bearer sk_id_…:sk_secret_…. Same claims in the body. See REST API → Tokens.

3. Connect + subscribe + publish

The wire is JSON over WebSocket. Same shape regardless of stack — Node example:

client.js
const WebSocket = require("ws");  // Browser: use the native WebSocket constructor (events via addEventListener instead of .on())

const token = "<jwt from step 2>";
const ws = new WebSocket(`wss://rms.metered.ca/v1?token=${encodeURIComponent(token)}`);

ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
console.log("←", msg);

// Server sends `welcome` first — auth succeeded. Subscribe.
if (msg.type === "welcome") {
ws.send(JSON.stringify({
type: "subscribe",
channel: "room-1",
requestId: "sub-1",
}));
}

// Server acks the subscribe — now we can publish.
if (msg.type === "ack" && msg.requestId === "sub-1") {
ws.send(JSON.stringify({
type: "publish",
channel: "room-1",
data: { hello: "world", from: "alice" },
requestId: "pub-1",
}));
}

// Anything published by another peer arrives as `message`.
if (msg.type === "message") {
console.log(`${msg.from} on ${msg.channel}:`, msg.data);
}
});

Run two copies with different sub values in their JWTs (e.g., alice and bob). They'll see each other's publishes.

What just happened

Five message types, one connection:

  • Outbound (you → server): subscribe, unsubscribe, publish, send
  • Inbound (server → you): welcome, ack, error, message, direct, presence

The protocol is straightforward enough to implement from scratch in an afternoon for any stack. The full spec is at Protocol → Wire Format.

Next steps

What you wantRead
Presence — who else is in this channelProtocol → Presence & Metadata
Direct (peer-to-peer) messages instead of broadcastProtocol → Wire Format → send
Per-message sender identity (peerMetadata)Protocol → Presence & Metadata
Server-side publish without a WebSocketREST API → Channels
Use-case-specific patterns (AI agents, IoT, chat)Use Case Guides
Limits & rate limitsLimits & Quotas
Adding WebRTC later5-Min Quickstart — WebRTC (drops to the SDK for JS apps)

Browser vs Node — same protocol, different API

The wire protocol is identical. The only difference is how you construct the WebSocket and listen for events:

  • Browser: new WebSocket(url) (built in), .addEventListener("message", …)
  • Node: npm install wsconst WebSocket = require("ws"), .on("message", …)

See the existing Introduction → Quickstart for both side by side.