Full Nori A3 docs coming soon. What works today.
Skip to content

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.

ts
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 channel

Video now flows into your <video> element and onTelemetry fires ~50×/s.

Video only

"Watch the robot" in its entirety — no jog, no telemetry:

ts
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:

ts
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

If start() never reaches connected, it's almost certainly the network: Connectivity.

Apache-2.0