Quickstart
Build a Next.js app that shows a building and a live occupancy sensor in 3D. About fifteen minutes.
You will build a small twin of an exhibition hall in Thessaloniki: the hall itself, a counter at its entrance that reports how many people are inside, and a 3D view you can click. The readings come from a fixture source, a stand in for a real sensor that you swap later without touching the rest of the code.
Before you start
You need Node.js 18 or newer and pnpm (or npm), and access to the Klorad packages: an npm
account that Prieston Technologies has added to the @klorad organisation under your
licence. Install explains how access works.
Sign in to npm
npm login
npm view @klorad/api versionIf the second command prints a version, your account can install the packages. If it says the package is not found, your account has not been given access yet.
Create the app
Make a folder my-twin with this package.json:
{
"name": "my-twin",
"private": true,
"scripts": {
"dev": "next dev -p 3100"
},
"dependencies": {
"@klorad/api": "^0.2.0",
"@klorad/engine-three": "^0.2.0",
"@klorad/react": "^0.2.0",
"next": "15.5.7",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"three": "^0.170.0"
},
"devDependencies": {
"@types/node": "^20.11.30",
"@types/react": "^19.0.0",
"typescript": "^5.9.0"
}
}and a root layout at my-twin/app/layout.tsx:
import type { ReactNode } from "react";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body style={{ margin: 0, fontFamily: "system-ui, sans-serif" }}>{children}</body>
</html>
);
}Describe the twin
Create my-twin/app/twin.tsx. Read it top to bottom: the provider creates a scene
anchored on Earth, each SceneObject declares something that exists, the Connector starts
the data source, and ThreeView draws it all.
"use client";
import { useMemo, useState } from "react";
import { fixtureSource } from "@klorad/api/integration";
import { Connector, KloradProvider, SceneObject, useLatest } from "@klorad/react";
import { ThreeView } from "@klorad/engine-three/react";
function Occupancy() {
const latest = useLatest("entrance", "occupancy");
return <p>{latest ? `${latest.value} ${latest.unit} at the entrance` : "Waiting for the first reading"}</p>;
}
export default function Twin() {
const [selected, setSelected] = useState<string | null>(null);
const source = useMemo(
() => fixtureSource({ id: "gateway", channels: [{ entity: "entrance-01", quantity: "occupancy", unit: "people", value: (t) => 20 + (t % 7) }] }),
[],
);
return (
<KloradProvider options={{ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } }}>
<SceneObject id="hall" name="Exhibition hall" position={{ east: 0, north: 0 }} geometry={{ kind: "box", width: 24, depth: 14, height: 7 }} />
<SceneObject
id="entrance"
name="Entrance counter"
position={{ east: 16, north: -10 }}
geometry={{ kind: "cylinder", radius: 2, height: 4 }}
correspondence="shadow"
binding={{ source: "gateway", entity: "entrance-01" }}
/>
<Connector source={source} />
<Occupancy />
<div style={{ height: 480 }}>
<ThreeView selected={selected} onPick={setSelected} />
</div>
</KloradProvider>
);
}Three things to notice:
- Position is real.
east: 16, north: -10means sixteen metres east and ten south of the origin, which is a latitude and longitude. Every object is stored on Earth. - The entrance is a Digital Shadow. Its
bindingnames a source and an entity, so readings fromgatewayaboutentrance-01land on it and nowhere else. useLatestre-renders on change. It reads the current reading, ordered by when it was observed, and subscribes for you.
Show it on a page
Create my-twin/app/page.tsx:
import Twin from "./twin";
export default function Page() {
return (
<main style={{ padding: 24 }}>
<h1>My first twin</h1>
<Twin />
</main>
);
}Run it
From the my-twin folder:
pnpm install
pnpm devOpen http://localhost:3100: the hall and the counter appear in 3D, and the line above them counts people, updated every second. Click an object to select it.
What you built
A scene with one Digital Object (the hall, which nothing physical updates) and one Digital Shadow (the counter, fed one way by timestamped readings). That split is the heart of Klorad: every object says how it corresponds to the physical world, and the types stop you from feeding readings to something that has no physical counterpart.
Run it liveSandbox: Digital ShadowTimestamped readings from a fixture source: late arrivals, duplicates and staleness handled by the Time Spectrum.Next
Replace the fixture with a real API
Poll a vendor endpoint, validate what comes back, and route it to your shadows.
Colour objects by their readings
Map state to colour and height with objectStyle.
Understand the model
Three layers, four pillars, and why the API is shaped by them.
Let people act on the twin
Actions, Entitlement, Behaviours, and commands to a device.
Something wrong or unclear? Tell your contact at Prieston Technologies.