Quickstart
Get your first real-time data integration running.
In this tutorial you'll replay a recorded event stream to drive a 3dverse scene. Once everything works, replacing the recording with MQTT, OPC UA, Azure Event Hubs, or another live source only requires changing the transport configuration.
What you need
- Node.js 18 or later
- A 3dverse scene — it can stay empty, the example creates the entities it drives
- A cube mesh asset in the same project, so those entities have something to display
- A token with access to the scene
The scene ID, the mesh UUID and the token all come from the 3dverse Console. See access control for what a token grants.
The agent below runs on Node.js, but the package also runs in the browser — see where each transport runs.
Step 1 — Install
npm install @3dverse/livelink-agent
See the Livelink Agent API reference for complete documentation.
This tutorial uses the built-in playback transport, so no additional packages are required.
Step 2 — Define How Events Update the Scene
An Event Mapping describes how incoming events affect your scene. For each event it answers three questions:
- Which events should be processed? —
channel,when - Which entity should be updated? —
entities - What should be written to that entity? —
updates
import type { EventMapping } from "@3dverse/livelink-agent";
// The UUID of the cube Mesh asset you copied from the Asset Browser.
const CUBE_MESH_ID = process.env.MESH_ID!;
// An Event Mapping describes how incoming events affect your scene.
// API reference: /references/livelink.agent/type-aliases/EventMapping
const telemetryMapping: EventMapping = {
// Only handle events on this topic pattern. `+` matches one segment.
channel: "devices/+/telemetry",
// No entity exists yet: create one per device id, the first time we see it.
entities: {
spawn: {
name: "device-{id}",
components: {
local_transform: { position: [0, 0, 0] },
mesh_ref: { value: CUBE_MESH_ID },
},
options: { delete_on_client_disconnection: true },
},
},
// What one event does. The device id comes out of the topic.
updates: event => {
const { pos } = event.payload as { pos: [number, number, number] };
return {
id: event.channel.split("/")[1],
update: { local_transform: { position: pos } },
};
},
};
The first time a device appears, an entity is created automatically. Every subsequent event simply updates its position.
What {id} is
updates returns an id alongside what to write. That id is how the mapping knows one device from another —
here it is the second segment of the topic, so an event on devices/dev-01/telemetry has the id dev-01.
spawn uses that same id: {id} in name is a placeholder replaced with it, so the two devices in the recording
become entities named device-dev-01 and device-dev-02. An id is only ever spawned once — the first event for
dev-01 creates the entity, and every later event resolves to that same entity and moves it.
Nothing in the code declares how many devices there are: the stream does. A broker with fifty devices publishing on that topic pattern produces fifty entities, with this mapping unchanged.
delete_on_client_disconnection: true means the entities vanish when the agent stops, leaving the scene as it was
found — convenient while you iterate.
Step 3 — Replay Sample Data
playback replays a dump of an event stream. The dump can live in a file or at a URL, but it can equally be an array
you already have in memory, which is all we need here:
const RECORDING = [
{ channel: "devices/dev-01/telemetry", timestamp: "2026-01-01T00:00:00.000Z", payload: { pos: [0, 1, 0] } },
{ channel: "devices/dev-02/telemetry", timestamp: "2026-01-01T00:00:00.000Z", payload: { pos: [2, 1, 0] } },
{ channel: "devices/dev-01/telemetry", timestamp: "2026-01-01T00:00:00.500Z", payload: { pos: [0, 1.5, 0] } },
{ channel: "devices/dev-02/telemetry", timestamp: "2026-01-01T00:00:00.500Z", payload: { pos: [2, 0.5, 0] } },
{ channel: "devices/dev-01/telemetry", timestamp: "2026-01-01T00:00:01.000Z", payload: { pos: [0, 1, 0] } },
{ channel: "devices/dev-02/telemetry", timestamp: "2026-01-01T00:00:01.000Z", payload: { pos: [2, 1, 0] } },
];
Messages are paced from their own timestamps, and the recording loops when it ends — so those six events make two cubes bob up and down indefinitely.
Step 4 — Connect Everything
Create an Agent, a Pipeline, and a SceneIngestion. SceneIngestion joins the sessions of a scene, binds each one to
the pipeline, and starts the sources:
import { Agent, IngestionPipeline, SceneIngestion } from "@3dverse/livelink-agent";
const ingestion = new SceneIngestion({
// The agent attaches to the scene's sessions.
agent: new Agent({
config: {
scene_id: process.env.SCENE_ID!,
token: process.env.TOKEN!,
// Join the session you already have open, or start one if there is none.
mode: "join-or-start",
// Sessions this agent starts are transient: nothing it writes is persisted,
// so a public token is enough to drive them.
is_transient: true,
// A session costs money for as long as it is open. Leave one minute after
// the last viewer disconnects, rather than holding it open for nobody.
leave_on_condition: { after_seconds: 60 },
},
}),
// The engine running your mappings.
pipeline: new IngestionPipeline({
mappings: telemetryMapping,
onError: error => console.error("[mapping]", error),
}),
// Where the events come from.
sources: [{ kind: "playback", config: { source: RECORDING } }],
});
ingestion.addEventListener("on-error", ({ error }) => console.error("[ingestion]", error));
ingestion.addEventListener("on-session-bound", ({ livelink }) =>
console.log(`Driving session ${livelink.session.session_id}`),
);
await ingestion.start();
Step 5 — Run It
SCENE_ID=<your-scene-id> MESH_ID=<your-cube-mesh-id> TOKEN=<your-token> npx tsx ingest.ts
Open the scene in the console — or in your own app — and you should see two cubes appear and start moving. The agent joined the session you are looking at, or created one you can join.
Step 6 — Replace Playback with Live Data
This is the payoff:
- The Event Mapping doesn't change.
- The Agent doesn't change.
- The Pipeline doesn't change.
- Only the transport changes.
// npm install mqtt
sources: [
{
kind: "mqtt",
config: {
broker_url: "mqtt://broker.example.com:1883",
topics: ["devices/+/telemetry"],
},
},
],
Because playback preserves the channel each message was recorded on, a mapping that selected
"devices/+/telemetry" in the replay selects exactly the same events live. That is what makes recording a few seconds
of a real stream the fastest way to build a mapping.
The same one-line swap works for OPC UA, Azure Event Hubs, or a transport you write yourself.
Stopping the Agent
process.on("SIGINT", async () => {
await ingestion.stop();
process.exit(0);
});
stop() stops the sources, leaves every session, and unbinds the scenes, in that order.
Next steps
- Mapping events to entities — addressing entities that already exist, handling events that carry several objects at once, deleting and hiding.
- Data sources — what each transport installs, where it runs, and what it
puts in an event's
channel. - Running an agent — which sessions to join, and the update rate that decides whether the motion looks smooth.