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.
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 JWTkidheader - 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.
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:
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 want | Read |
|---|---|
| Presence — who else is in this channel | Protocol → Presence & Metadata |
| Direct (peer-to-peer) messages instead of broadcast | Protocol → Wire Format → send |
Per-message sender identity (peerMetadata) | Protocol → Presence & Metadata |
| Server-side publish without a WebSocket | REST API → Channels |
| Use-case-specific patterns (AI agents, IoT, chat) | Use Case Guides |
| Limits & rate limits | Limits & Quotas |
| Adding WebRTC later | 5-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 ws→const WebSocket = require("ws"),.on("message", …)
See the existing Introduction → Quickstart for both side by side.