Quick start
The fastest path: use the reference Supabase transport with the room + Supabase credentials we provision for you. A real robot's room is a private channel gated by Supabase RLS — sign the supabase client in as the robot's paired account and you're admitted; there is no room token to pass. Pairing is a one-time step, upstream of the SDK — see Install.
import { RemoteTeleop } from "@nori/sdk";
import { SupabaseSignaling } from "@nori/sdk/supabase";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); // values we give you
const video = document.querySelector("video")!;
const teleop = new RemoteTeleop({
signaling: new SupabaseSignaling(supabase, ROOM, (...m) => console.log(...m)),
videoEl: video,
// No room token: a real robot's private room is gated by Supabase RLS (sign `supabase`
// in as the paired account). Pass `{ private: true }` to SupabaseSignaling for a real
// robot; open dev rooms (`nori-dev`) use the default public join.
stun: "stun:stun.l.google.com:19302",
// TURN is only needed on strict networks. Credentials are MINTED per session — fetch
// GET /api/v1/turn/credentials with your account's JWT and pass the result through as
// turnUrls / turnUser / turnCred. Static credentials no longer authenticate.
// See "Connectivity".
forceRelay: false,
arm: "right", // which arm keyboard/jog drives
onLog: (m) => console.log(m),
onConnState: (s) => console.log("conn:", s),
onTelemetry: (t) => { // live: loopHz, safety, tempC, per-joint state{}, lift height mm…
console.log("safety:", t.safety, "state:", t.state);
},
onMode: (mode) => console.log("mode:", mode),
onControlActive: (a) => console.log("control:", a),
});
await teleop.start(); // subscribes, answers the robot's offer, opens the control channelVideo now flows into your <video> element and onTelemetry fires ~50×/s.
Video only
"Watch the robot" in its entirety — no jog, no telemetry:
import { RemoteTeleop } from "@nori/sdk";
import { SupabaseSignaling } from "@nori/sdk/supabase";
const teleop = new RemoteTeleop({
signaling: new SupabaseSignaling(supabase, room, console.log),
videoEl: document.querySelector("video")!,
stun, turnUrls, turnUser, turnCred, forceRelay: false,
arm: "right", onLog: console.log, onConnState: console.log,
onTelemetry: () => {}, onMode: () => {}, onControlActive: () => {},
});
teleop.start(); // video appears in the element when connected
// teleop.stop() to tear down.Develop without a robot
@nori/sdk/mock ships a fake robot — zero hardware, zero cloud, zero network. It runs the real SDK path (handshake, loopback WebRTC with synthetic video, telemetry, jog), so code developed against it talks to a real Nori unchanged:
import { RemoteTeleop } from "@nori/sdk";
import { createMockRobot } from "@nori/sdk/mock";
const robot = createMockRobot();
const teleop = new RemoteTeleop({ signaling: robot.signaling /* ...options as usual... */ });
await teleop.start();Honest limits: no audio, no perception frames (use injectPerception()), and motion is plausible rather than kinematically true — never validate motion or train on mock trajectories. createMockRobot() needs a browser (WebRTC + canvas); MockDaemonSim alone runs anywhere, including Node CI.
Next
- Driving the robot — keyboard, programmatic jog, commands.
- The handshake — what the robot tells you about itself on connect.
- The safety contract — read this before you ship.
If start() never reaches connected, it's almost certainly the network: Connectivity.
