# Physical correspondence

Source: https://docs.klorad.com/concepts/correspondence

Digital Object, Digital Shadow, Digital Twin. Every Scene Object says how it relates to the physical world.

"Digital twin" is used loosely for anything in 3D. Klorad is strict about it, because the
difference decides what code may do with an object. Every Scene Object has a
`correspondence`, and there are three.

| Kind | `correspondence` | Has a `binding` | Accepts readings | Accepts commands |
| --- | --- | --- | --- | --- |
| Digital Object | `"object"` (the default) | no | no | no |
| Digital Shadow | `"shadow"` | yes | yes | no |
| Digital Twin | `"twin"` | yes | yes | yes, through an actuator |

**A Digital Object** is modelled only. Nothing physical updates it: a building footprint, a
zone, a planned extension. It changes only when your code or a Behaviour changes it.

**A Digital Shadow** mirrors a physical thing one way. Its `binding` names a source and an
entity in that source, and readings from there become its state. Your code cannot command it,
because there is nothing to command: a footfall counter, a temperature probe, a bin fill sensor.

**A Digital Twin** mirrors a physical thing both ways. Readings flow in like a shadow's, and an
authorised Action can send a command back to the device. The twin's state still changes only
when the device reports it: Klorad never pretends a barrier is open because someone asked it to
open.

## The types enforce it

A shadow or a twin without a `binding` does not compile, and a spec that has a `binding` but
does not say `"shadow"` or `"twin"` does not compile either (and `add` throws on it in plain
JavaScript), so a sensor is never silently demoted to a plain object. At runtime, offering a
reading to a Digital Object returns `{ status: "rejected", reason: "not-observable" }`.
`isObservable(object)` narrows a `SceneObject` to a shadow or a twin.

```ts
scene.add({ name: "Probe", position: { east: 0, north: 0 }, binding: { source: "bms", entity: "t1" } });
// type error: a binding needs correspondence: "shadow" or "twin"
```

## Choosing

Start from the physical thing. If nothing reports on it, it is an object. If something reports
on it and you only watch, it is a shadow. Make it a twin only when you have a real path to
command it, and an [Action](https://docs.klorad.com/concepts/interaction) that uses that path.

---

# Overview

Source: https://docs.klorad.com/concepts

The ideas behind the Klorad API, one per page, in the order the code builds on them.

Klorad's API is not a collection of features. It is one model, taken from the doctoral thesis
behind Klorad, and every export maps to a class in it. Once you know the model, the API is
predictable: you can guess where a capability lives and what it may and may not do.

Read these pages in order the first time. Each one explains one idea, shows the smallest code
that uses it, and links to the sandbox page that runs it.

  - [The model](https://docs.klorad.com/concepts/model): Three layers and four pillars, and the rule about who may call whom.
  - [Space](https://docs.klorad.com/concepts/space): Every scene is anchored on Earth; positions are real places.
  - [The scene and its events](https://docs.klorad.com/concepts/scene): The one place world state changes, and the ordered stream every change becomes.
  - [Physical correspondence](https://docs.klorad.com/concepts/correspondence): Digital Object, Digital Shadow, Digital Twin: how each object relates to the physical world.
  - [Time](https://docs.klorad.com/concepts/time): Readings ordered by when they were observed, not when they arrived.
  - [Integration](https://docs.klorad.com/concepts/integration): Sources of readings and devices that accept commands.
  - [Interaction and actuation](https://docs.klorad.com/concepts/interaction): Action, Entitlement, Behaviour, Animation: one way, always.
  - [Worlds and access](https://docs.klorad.com/concepts/worlds): Entitled views of one twin, and the audit log.
  - [Rendering](https://docs.klorad.com/concepts/rendering): Renderers draw what World defines and never change it.

---

# Integration

Source: https://docs.klorad.com/concepts/integration

Sources report readings about their own entities; connectSource routes them to the shadows and twins bound to them.

The Integration layer couples the world to reality. It depends on World; World never imports
it. Its central idea is that a **source knows nothing about scenes**: it reports readings about
its own entities (`entrance-01`, `vms-12`, `barrier-1`), and the scene decides where they
belong through each object's `binding`.

```ts
binding: { source: "gateway", entity: "entrance-01" }
```

That keeps vendor code and world code apart. You can swap a fixture for a real gateway, or one
vendor for another, without touching a single Scene Object.

## Sources

An `ObservationSource` has an `id` (matched against `binding.source`) and a `start(sink)`
method that begins reporting and returns a function that stops it. Three ship today:

| Source | What it does | Use it for |
| --- | --- | --- |
| `fixtureSource` | Generates readings on an interval from functions you give it. | Demos, tests, the quickstart. Not a device. |
| `pollingSource` | Calls your `read()` on an interval and reports what it returns. | Any system that only offers a pull API. |
| `fixtureDevice` | A simulated device: reports state and accepts commands. | Demonstrating a Digital Twin's two way loop. |

Writing your own is one object with two members, so a push source (a webhook receiver, a
message queue consumer) is a few lines.

## Connecting

`connectSource(scene, source)` starts the source and routes every reading to every shadow or
twin bound to that source and entity. It returns a `Connection` whose `stats()` counts what
happened, which is the first thing to look at when data does not show up:

| Counter | Meaning |
| --- | --- |
| `received` | Readings the source reported. |
| `latest`, `late`, `duplicate` | What the scene did with them (see [Time](https://docs.klorad.com/concepts/time)). |
| `unbound` | No object is bound to that source and entity. Usually a typo in a binding. |
| `rejected` | The scene refused the reading, for example an invalid value. |

```ts title="connect-source.ts"
import { createScene } from "@klorad/api/world";
import { connectSource, fixtureSource, pollingSource } from "@klorad/api/integration";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({
  id: "entrance",
  name: "Entrance counter",
  position: { east: 16, north: -10 },
  correspondence: "shadow",
  binding: { source: "gateway", entity: "entrance-01" },
});

// A fixture: generated readings once a second. A demo of the data path, not a device.
const fixture = fixtureSource({
  id: "gateway",
  channels: [{ entity: "entrance-01", quantity: "occupancy", unit: "people", value: (tick) => 20 + (tick % 7) }],
});
const connection = connectSource(scene, fixture);

// A real system that only offers a pull API, for example a connector's getStatus.
interface SignStatus {
  message: string;
  updatedAt: string;
}
declare const signs: { getStatus(ids: string[]): Promise<Record<string, SignStatus>> };

const vms = pollingSource({
  id: "atms",
  intervalMs: 15_000,
  read: async () => {
    const status = await signs.getStatus(["vms-12", "vms-14"]);
    return Object.entries(status).map(([entity, s]) => ({
      entity,
      quantity: "message",
      value: s.message,
      observedAt: Date.parse(s.updatedAt),
    }));
  },
});

export { connection, vms };
```

## Outside data is not trusted

Validate every vendor payload at the boundary, inside `read()` or your push handler, before it
becomes a reading. [Connect a polling API](https://docs.klorad.com/guides/connect-a-polling-api) shows the pattern with
Zod. Credentials for vendor APIs belong on a server, never in browser code.

## Devices and commands

A Digital Twin's commands leave through an `Actuator`: an object with a `source` (matched against the twin's `binding.source`) and a
`send(command)` method that returns whether the device accepted it. `fixtureDevice` is both a
source and an actuator, so it closes the loop the way a real device does: it accepts a command,
and after a latency reports the new state as a reading. See
[Interaction and actuation](https://docs.klorad.com/concepts/interaction).

---

# Interaction and actuation

Source: https://docs.klorad.com/concepts/interaction

Action, Entitlement, Behaviour, Animation. How people change a twin, and how a twin commands a device, in one direction only.

A twin people can only look at is a dashboard. Interaction is how they change it, and for a
Digital Twin, how a change reaches the physical thing. Klorad models it as a chain of four
parts that runs in one direction.

| Part | What it is | Who defines it |
| --- | --- | --- |
| **Action** | Something that can be done to an object: "Dim", "Open", "Mark emptied". It says which objects offer it (`appliesTo`), how to check its parameters (`validate`), which Behaviour follows, and for a twin, which command to send. | You, as an `ActionDefinition` |
| **Entitlement** | The decision whether this actor may take this Action on this object in this World. World only asks; Access answers. | `@klorad/access` (`createAccessPolicy`), or `allowEveryone` for a single user tool |
| **Behaviour** | The predefined change that follows an allowed Action: it patches the object and may ask for an Animation. | You, as a `BehaviourDefinition` |
| **Animation** | A request to renderers to show the change (`pulse` or `highlight`). It changes nothing. | The Behaviour, through `animate()` |

## One way only

A Behaviour receives `update` and `animate`, and nothing that could request an Action. That is
deliberate: chains of Actions triggering Actions are how automation loops and privilege
escalation happen. If something should follow an Action, a person or a rule outside the chain
requests it, and the Entitlement checks it again.

A Behaviour's changes are collected and applied together after it returns. If it throws, or
produces an invalid change, nothing changes and the outcome is `failed`.

## Outcomes

`interaction.perform(request)` resolves to an `ActionResult`. Every outcome, refusals included,
is also one `action` event on the scene, so an audit log sees what was denied as well as what
was done.

| Outcome | Meaning |
| --- | --- |
| `done` | The Behaviour ran. |
| `sent` | A twin's command was accepted by its device, and the Behaviour ran. The twin's physical state follows when the device reports it. |
| `denied` | The Entitlement refused. `reason` says why. |
| `invalid` | Unknown Action or object, the Action does not apply to the object, or its parameters failed `validate`. |
| `failed` | The actuator refused or failed, or the Behaviour threw. World state is unchanged. |

## Commanding a Digital Twin

An Action with a `command` applies to twins. When it is allowed, Klorad builds a `Command`
(with the twin's entity, the actor and the instant) and sends it through the `Actuator`
registered for the twin's `binding.source`, before the Behaviour runs. If the device refuses,
the outcome is `failed` and nothing changes.

The twin's physical state is never written by the Action. It changes when the device reports
its new state as a reading, the same path every reading takes. Typically the Behaviour records
the request (`requestedAt`) and the reading confirms it.

```ts title="interaction.ts"
import { allowEveryone, createAccessPolicy } from "@klorad/access";
import { connectSource, fixtureDevice } from "@klorad/api/integration";
import { createInteraction, createScene } from "@klorad/api/world";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({ id: "lamp", name: "Street lamp", position: { east: -8, north: 4 }, properties: { level: 100 } });
scene.add({
  id: "barrier",
  name: "Entrance barrier",
  position: { east: 6, north: -2 },
  correspondence: "twin",
  binding: { source: "plc", entity: "barrier-1" },
});

// The physical side: a simulated PLC that reports the barrier's state and accepts commands.
const plc = fixtureDevice({
  id: "plc",
  entities: { "barrier-1": { state: "closed" } },
  respond: (command, now) => (now.state === command.name ? `already ${now.state}` : { state: command.name }),
});
connectSource(scene, plc);

const policy = createAccessPolicy({
  scene,
  roles: { operator: "*", visitor: [] },
  worlds: [{ id: "site", name: "Site", scope: { all: true }, capabilities: { actions: "*" }, principals: [{ kind: "role", id: "operator" }] }],
});

const interaction = createInteraction(scene, {
  entitlement: policy,
  actuators: [plc],
  behaviours: [
    { id: "dim", run: ({ update, animate }) => (update({ properties: { level: 30 } }), animate({ kind: "pulse" })) },
    { id: "requested", run: ({ update, now }) => update({ properties: { requestedAt: now } }) },
  ],
  actions: [
    { id: "dim", label: "Dim", appliesTo: (o) => o.id === "lamp", behaviour: "dim" },
    { id: "open", label: "Open", appliesTo: (o) => o.correspondence === "twin", behaviour: "requested", command: () => ({ name: "open" }) },
  ],
});

const operator = { id: "eleni", roles: ["operator"] };
const visitor = { id: "guest", roles: ["visitor"] };

async function demo() {
  await interaction.perform({ action: "dim", objectId: "lamp", actor: visitor, world: "site" }); // { outcome: "denied", reason: ... }
  await interaction.perform({ action: "dim", objectId: "lamp", actor: operator, world: "site" }); // { outcome: "done" }
  await interaction.perform({ action: "open", objectId: "barrier", actor: operator, world: "site" }); // { outcome: "sent", command }
  // About 600 ms later the PLC reports state "open": scene.latest("barrier", "state")?.value === "open"
  return interaction.available("lamp", operator, "site").map((a) => a.id); // ["dim"]
}

// For a demo or a single-user tool, allowEveryone says so in its name.
const playground = createInteraction(createScene({ coordinateSystem: { origin: { lat: 0, lon: 0 } } }), { entitlement: allowEveryone });

export { demo, playground };
```

In React, wrap your tree in `<InteractionProvider interaction={interaction}>`; `useActions` lists
what an actor may do on an object and `useAction` performs one and tracks its state. See
[@klorad/react](https://docs.klorad.com/reference/react).

---

# The model

Source: https://docs.klorad.com/concepts/model

Three layers, four pillars, and the rule that decides who may call whom.

Klorad follows the system model of the doctoral thesis *Spatial Computing and Geospatial
Science for Virtual Worlds and Digital Twins* (University of Macedonia). The model has three
layers, which say where code lives, and four pillars, which say what a twin must get right.

## Three layers

**World** owns meaning: the scene, the objects in it, how each corresponds to the physical
world, the readings that describe it, and the rules that change it. It is pure TypeScript. It
does not import React, a renderer, a database or the network, so the same world runs in a
browser, on a server and in a test.

**Access** is how people and devices reach the world: who may enter which view of it, and who
may take which Action. It lives in `@klorad/access`.

**Integration** couples the world to reality: sensors, vendor APIs, devices that accept
commands. It lives in `@klorad/api/integration`.

Access and Integration depend on World. World never depends on them: when it needs an answer
only Access can give (may this person do this?), it defines a small interface and Access
implements it. That is why `createInteraction` asks for an `entitlement` instead of importing
one.

Renderers and React bindings sit outside the layers. They read the world and draw it; they
never change it. An app that wants to change world state calls the scene.

## Four pillars

| Pillar | What it guarantees | Page |
| --- | --- | --- |
| **Space** | Every scene has an Earth anchored coordinate system, so every position is a real place. | [Space](https://docs.klorad.com/concepts/space) |
| **Time** | Readings are ordered by when they were observed. A late reading never overwrites a newer state. | [Time](https://docs.klorad.com/concepts/time) |
| **Physical correspondence** | Every object declares whether it is modelled only, mirrored one way, or mirrored both ways. | [Physical correspondence](https://docs.klorad.com/concepts/correspondence) |
| **Interaction and actuation** | An Action is checked by an Entitlement, then a Behaviour changes state and an Animation shows it. Never backwards. | [Interaction](https://docs.klorad.com/concepts/interaction) |

The code was built in that order, because each pillar needs the one before it: there is no
meaningful time without a place, no correspondence without time, and no safe actuation without
correspondence.

## From thesis class to export

| Thesis class | Export | Package |
| --- | --- | --- |
| 3D Scene | `createScene`, `Scene` | `@klorad/api/world` |
| Coordinate System | `createCoordinateSystem`, `CoordinateSystem` | `@klorad/api/world` |
| Scene Object | `SceneObject`, `SceneObjectSpec` | `@klorad/api/world` |
| Digital Object, Shadow, Twin | `DigitalObject`, `DigitalShadow`, `DigitalTwin` | `@klorad/api/world` |
| Time Spectrum | `Clock`, `Observation`, `compareObservations` | `@klorad/api/world` |
| IoT Devices, APIs | `ObservationSource`, `connectSource`, `fixtureSource`, `pollingSource` | `@klorad/api/integration` |
| Action, Behaviour, Animation | `ActionDefinition`, `BehaviourDefinition`, `AnimationRequest`, `createInteraction` | `@klorad/api/world` |
| Entitlement System | `Entitlement` (asked by World), `createAccessPolicy` (answers) | `@klorad/api/world`, `@klorad/access` |
| Actuation of a Twin | `Actuator`, `Command`, `fixtureDevice` | `@klorad/api/world`, `@klorad/api/integration` |
| Worlds (entitled views) | `World`, `inScope`, `projectScene`, audit log | `@klorad/access`, `@klorad/api/world` |
| Rendering | `createThreeRenderer`, `ThreeView` | `@klorad/engine-three` |

A new export needs a class in this table or a recorded architecture decision behind it. That
rule keeps the API small and keeps it meaning one thing.

---

# Rendering

Source: https://docs.klorad.com/concepts/rendering

Renderers draw what World defines, follow its events, and never change world state.

A renderer is a reader. It draws every Scene Object, follows the scene's events to stay in
step, and reports what the user picked. It never writes to the world: a click becomes an id
passed to your code, and your code decides what, if anything, changes.

`@klorad/engine-three` draws with three.js. It is the renderer of 0.1.

## Frames

The scene's local frame (east, north, up in metres) maps to three.js as **east is +x, up is
+y, north is -z**. `localToThree`, `threeToLocal` and `geoToThree` convert for code that adds
its own three.js objects.

## What it shows by default

With no style function, colour encodes correspondence (objects, shadows and twins each have
their own) and freshness (a shadow whose readings have gone stale is drawn in the stale
colour). A newer reading or a Behaviour's Animation makes the object flash. Objects with
`geometry: { kind: "model", uri }` load a glTF model.

## Your own looks

`objectStyle` maps an object's state to how it looks: colour, opacity and a height scale. It
receives the object, its current readings, whether they are stale, and the palette.

```ts title="render.ts"
import { createScene } from "@klorad/api/world";
import { createThreeRenderer } from "@klorad/engine-three";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({ name: "Exhibition hall", position: { east: 0, north: 0 }, geometry: { kind: "box", width: 24, depth: 14, height: 7 } });

const renderer = createThreeRenderer({
  container: document.getElementById("view")!,
  scene,
  onPick: (id) => renderer.select(id),
  objectStyle: ({ object, readings, stale, palette }) => {
    if (stale) return { color: palette.stale };
    const people = readings.occupancy?.value;
    return typeof people === "number" ? { color: palette.shadow, heightScale: people / 20 } : { color: object.color ?? palette.object };
  },
});
renderer.frame();
```

In React, `ThreeView` from `@klorad/engine-three/react` takes the same options and reads the
scene from the nearest `KloradProvider`. See
[Style objects by readings](https://docs.klorad.com/guides/style-objects-by-readings).

---

# The scene and its events

Source: https://docs.klorad.com/concepts/scene

The one place world state changes, and the ordered stream of events every change becomes.

A `Scene` holds the Scene Objects of one twin and the readings that describe them. It is the
only way to change world state: `add`, `update`, `remove` and `observe` are the four writes,
and everything else reads.

## Every change is one event

Each write emits exactly one `SceneEvent` with a scene wide sequence number `seq`, starting at
1 and strictly increasing, and the instant it happened on the scene's clock. Subscribers see
the same events in the same order: a React component, a renderer and an audit log never
disagree about what happened first.

```ts title="scene-objects.ts"
import { createScene, type DigitalShadow } from "@klorad/api/world";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });

// A Digital Object: no physical counterpart is synchronised.
const hall = scene.add({
  id: "hall",
  name: "Exhibition hall",
  position: { east: 0, north: 0 },
  geometry: { kind: "box", width: 24, depth: 14, height: 7 },
});

// A Digital Shadow: one-way, timestamped flow from a physical sensor.
const entrance: DigitalShadow = scene.add({
  id: "entrance",
  name: "Entrance counter",
  position: { lat: 40.62629, lon: 22.94857 },
  geometry: { kind: "cylinder", radius: 2, height: 4 },
  correspondence: "shadow",
  binding: { source: "gateway", entity: "entrance-01" },
});

scene.update("hall", { orientation: { heading: 30 }, properties: { floor: 0 } });

const log: string[] = [];
const stop = scene.subscribe((event) => log.push(`#${event.seq} ${event.type}`));

export { hall, entrance, stop, log };
```

| Event | Emitted when |
| --- | --- |
| `object.added` | An object is added. Carries the object. |
| `object.updated` | An object changes. Carries the new object and the `previous` one. |
| `object.removed` | An object is removed, with its readings. |
| `observation` | A reading is accepted. `latest` says whether it became the current state. |
| `action` | An Action was requested, with its outcome, allowed or not. |
| `animation` | A Behaviour asked renderers to show a change. |

A reading seen twice emits nothing; an update that throws emits nothing. If a subscriber
throws, the others still receive the event and the change still stands: pass `onListenerError`
to `createScene` to hear about it.

## Objects are values

A Scene Object is a frozen value. `update` does not mutate it; it builds a new object and emits
the old and the new one. `objects()` and `readings(id)` return the same reference until
something changes, so React can compare by reference and skip work.

An update merges `properties` and replaces everything else it names. How an object corresponds
to the physical world (its `correspondence` and `binding`) cannot be patched: remove the object
and add it again, which makes the change explicit in the event stream.

## Ids

Give objects stable ids of your own (`"hall"`, `"entrance"`) when other code refers to them:
bindings, Worlds and alert rules all do. If you omit `id`, the scene assigns `o1`, `o2` and so
on. Adding an id twice throws.

---

# Space

Source: https://docs.klorad.com/concepts/space

Every scene is anchored on Earth. Positions are real places, and renderers still work in metres.

A Klorad scene cannot exist without a coordinate system, and `createScene` refuses to create
one without it. The coordinate system is an **East, North, Up** frame in metres, anchored at a
point on the WGS84 ellipsoid, the same model GPS uses.

That gives you both things a twin needs: positions that are real places (you can hand them to
a map, a survey or a GPS) and a flat local frame in metres that a renderer can draw without
thinking about the curvature of the Earth.

```ts title="create-scene.ts"
import { createScene } from "@klorad/api/world";

const scene = createScene({
  name: "Waterfront",
  coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838, height: 0 } },
});

const cs = scene.coordinateSystem;
const tower = cs.toLocal({ lat: 40.62637, lon: 22.94838, height: 0 }); // { east: 0, north: 0, up: 0 }
const rotunda = cs.toLocal({ lat: 40.63333, lon: 22.95278, height: 0 }); // about 372 m east, 773 m north
const back = cs.toGeo({ east: 100, north: 50, up: 0 }); // a GeoPoint again

export { scene, tower, rotunda, back };
```

## Two ways to give a position

Every position may be given either way, and both are stored on Earth:

| Form | Fields | Use it when |
| --- | --- | --- |
| On Earth | `lat`, `lon`, `height` | The position comes from GPS, a map or a survey. |
| Local | `east`, `north`, `up` (metres from the origin) | You are laying out a site by hand or from a drawing. |

`height` and `up` are optional and default to zero. Heights are metres above the ellipsoid, not
above sea level.

## How accurate it is

The conversion goes through Earth centred coordinates and back, so it holds over the whole
globe, not only near the origin. Round trips are accurate well below a millimetre; the
[Space sandbox](https://docs.klorad.com/sandbox/space) measures it live on landmarks spread over two kilometres.

Choose an origin near the middle of your site. The local frame is flat, so distances measured
in it drift slowly from true distances on the ground as you move away from the origin; across a
campus or a district the difference does not matter.

---

# Time

Source: https://docs.klorad.com/concepts/time

Readings are ordered by when they were observed, not when they arrived. Late data never overwrites a newer state.

Sensors and vendor APIs do not deliver in order. A gateway buffers, a mobile network drops and
retries, a poll returns yesterday's value with today's response. If a twin takes the last
reading that arrived as the truth, it will sometimes show the past as the present. Klorad's
Time Spectrum exists to make that impossible.

## Two instants per reading

Every observation carries two instants:

- `observedAt`: when the physical world was in that state. The source supplies it.
- `receivedAt`: when Klorad learnt about it. The scene stamps it from its clock.

The current state of a quantity is the reading with the latest `observedAt` (ties broken by the
source's `seq`). Arrival order decides nothing.

## Latest, late, duplicate, stale

`scene.observe` returns what happened to each reading:

| Status | Meaning | Event |
| --- | --- | --- |
| `latest` | Newer than the current state: it becomes the current state. | `observation`, `latest: true` |
| `late` | Older than the current state: it joins the history, in order, and changes nothing else. | `observation`, `latest: false` |
| `duplicate` | The same reading again: same source, quantity and `observedAt`, and the same `seq` (or the same value when there is no `seq`). Ignored. | none |
| `rejected` | Unknown object, a Digital Object, or an invalid reading. | none |

A reading is **stale** when it is older than the object's `staleAfterMs` (or the scene's,
five minutes by default). Staleness is evaluated against the scene's clock when you ask (`scene.isStale`), so a
sensor that goes quiet turns stale without any event. The three.js renderer re-checks it as
time passes; in React, `useShadow(id).isStale(quantity)` evaluates it at render time.

```ts title="observations.ts"
import { createManualClock, createScene } from "@klorad/api/world";

const clock = createManualClock(Date.parse("2026-10-09T10:00:00Z"));
const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } }, clock, staleAfterMs: 60_000 });
scene.add({
  id: "climate",
  name: "Hall climate",
  position: { east: -6, north: 2, up: 7 },
  correspondence: "shadow",
  binding: { source: "bms", entity: "ahu-3" },
});

const t = clock.now();
const reading = (observedAt: number, value: number, seq: number) =>
  scene.observe({ objectId: "climate", quantity: "temperature", unit: "°C", value, observedAt, seq, source: "bms" });

reading(t - 2_000, 21.8, 41).status; // "latest"
reading(t - 1_000, 22.0, 42).status; // "latest"
reading(t - 1_500, 21.9, 43).status; // "late": kept in the history, does not replace 22.0
reading(t - 1_000, 22.0, 42).status; // "duplicate": nothing changes, no event

scene.latest("climate", "temperature")?.value; // 22.0
scene.history("climate", "temperature").length; // 3
clock.advance(61_000);
scene.isStale("climate", "temperature"); // true
```

## Clocks

A scene reads time from a `Clock`. The default is the system clock. `createManualClock` gives
you one you advance yourself, which makes tests of staleness and ordering exact; see
[Test with a manual clock](https://docs.klorad.com/guides/test-with-a-manual-clock).

The history keeps the last 500 readings per object and quantity by default
(`historyLimit` on `createScene`).

---

# Worlds and access

Source: https://docs.klorad.com/concepts/worlds

One twin, many entitled views. A World decides which objects a group sees, what it may do, and who may enter.

A site has an operations room, maintenance crews, contractors and sometimes the public. They
look at the same twin, but each should see only what is theirs and do only what they are
entitled to. Klorad does not make a copy of the twin per audience; it gives each audience a
**World**.

## What a World is

A World is a named, entitled view over one twin:

| Field | Decides |
| --- | --- |
| `scope` | Which objects it contains: all of them, a list of ids, objects of a given correspondence, objects whose properties match, or objects within a radius of a point. |
| `capabilities.actions` | Which Actions may be taken in it: `"*"` or a list of ids. |
| `principals` | Who may enter: users, teams or roles. An empty list means nobody. |
| `presentation` | How it looks: colours, an initial camera, whether it is for signed in users or public. |

## Entitlement is an intersection

An actor may take an Action on an object when **all** of these hold:

1. one of the actor's organisation roles allows the Action;
2. the request names a World and the actor may enter it;
3. the object is inside the World's scope;
4. the World's capabilities allow the Action.

Any one failing denies the request, and the `reason` says which. `createAccessPolicy` returns
an `AccessPolicy` that is the `Entitlement` the World layer asks.

## Members receive a projection

`policy.view(worldId, actor)` returns a projection: a scene that contains only the in scope
objects and their readings, kept in step with the twin as it changes. Give that scene to the
member's interface and nothing outside the World ever reaches their browser. An object that
moves out of scope, or whose properties stop matching, disappears from the view.

```ts title="worlds.ts"
import { auditActions, createAccessPolicy, createMemoryAuditLog, type World } from "@klorad/access";
import { createScene } from "@klorad/api/world";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({ id: "n1", name: "Bin N1", position: { east: -30, north: 70 }, correspondence: "shadow", binding: { source: "bins", entity: "n1" }, properties: { district: "north" } });
scene.add({ id: "s1", name: "Bin S1", position: { east: -25, north: -80 }, correspondence: "shadow", binding: { source: "bins", entity: "s1" }, properties: { district: "south" } });

const worlds: World[] = [
  { id: "city", name: "City operations", scope: { all: true }, capabilities: { actions: "*" }, principals: [{ kind: "role", id: "admin" }] },
  {
    id: "north",
    name: "North crew",
    scope: { properties: { district: "north" } },
    capabilities: { actions: ["mark-emptied"] },
    principals: [{ kind: "team", id: "crew-north" }],
    presentation: { primary: "#0e7490", visibility: "authenticated" },
  },
];

const audit = createMemoryAuditLog();
const policy = createAccessPolicy({ scene, roles: { admin: "*", crew: ["mark-emptied"] }, worlds, audit });
auditActions(scene, audit);

const kostas = { id: "kostas", roles: ["crew"], teams: ["crew-north"] };
const entered = policy.worldsFor(kostas).map((w) => w.name); // ["North crew"]

// What Kostas receives: a scene with Bin N1 only, kept in step with the twin.
const view = policy.view("north", kostas);
const visible = view.scene.objects().map((o) => o.id); // ["n1"]

export { entered, visible };
```

## The audit log

`auditActions(scene, sink)` writes every Action outcome, allowed or denied, to an append only
sink, and the policy writes every World change made through `setWorld` and `removeWorld`.
`createMemoryAuditLog` is the in memory sink for demos and tests; a server app implements
`AuditSink` over a database table.

---

# Act on a twin

Source: https://docs.klorad.com/guides/act-on-a-twin

Define an Action that sends a command to a device, check who may take it, and watch the twin follow the device.

This guide makes an entrance barrier operable: operators may open it, visitors may not, and the
twin shows the barrier open only once the device says it is.

```ts title="interaction.ts"
import { allowEveryone, createAccessPolicy } from "@klorad/access";
import { connectSource, fixtureDevice } from "@klorad/api/integration";
import { createInteraction, createScene } from "@klorad/api/world";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({ id: "lamp", name: "Street lamp", position: { east: -8, north: 4 }, properties: { level: 100 } });
scene.add({
  id: "barrier",
  name: "Entrance barrier",
  position: { east: 6, north: -2 },
  correspondence: "twin",
  binding: { source: "plc", entity: "barrier-1" },
});

// The physical side: a simulated PLC that reports the barrier's state and accepts commands.
const plc = fixtureDevice({
  id: "plc",
  entities: { "barrier-1": { state: "closed" } },
  respond: (command, now) => (now.state === command.name ? `already ${now.state}` : { state: command.name }),
});
connectSource(scene, plc);

const policy = createAccessPolicy({
  scene,
  roles: { operator: "*", visitor: [] },
  worlds: [{ id: "site", name: "Site", scope: { all: true }, capabilities: { actions: "*" }, principals: [{ kind: "role", id: "operator" }] }],
});

const interaction = createInteraction(scene, {
  entitlement: policy,
  actuators: [plc],
  behaviours: [
    { id: "dim", run: ({ update, animate }) => (update({ properties: { level: 30 } }), animate({ kind: "pulse" })) },
    { id: "requested", run: ({ update, now }) => update({ properties: { requestedAt: now } }) },
  ],
  actions: [
    { id: "dim", label: "Dim", appliesTo: (o) => o.id === "lamp", behaviour: "dim" },
    { id: "open", label: "Open", appliesTo: (o) => o.correspondence === "twin", behaviour: "requested", command: () => ({ name: "open" }) },
  ],
});

const operator = { id: "eleni", roles: ["operator"] };
const visitor = { id: "guest", roles: ["visitor"] };

async function demo() {
  await interaction.perform({ action: "dim", objectId: "lamp", actor: visitor, world: "site" }); // { outcome: "denied", reason: ... }
  await interaction.perform({ action: "dim", objectId: "lamp", actor: operator, world: "site" }); // { outcome: "done" }
  await interaction.perform({ action: "open", objectId: "barrier", actor: operator, world: "site" }); // { outcome: "sent", command }
  // About 600 ms later the PLC reports state "open": scene.latest("barrier", "state")?.value === "open"
  return interaction.available("lamp", operator, "site").map((a) => a.id); // ["dim"]
}

// For a demo or a single-user tool, allowEveryone says so in its name.
const playground = createInteraction(createScene({ coordinateSystem: { origin: { lat: 0, lon: 0 } } }), { entitlement: allowEveryone });

export { demo, playground };
```

## Step by step

1. **The barrier is a twin.** `correspondence: "twin"` with a binding to the PLC that controls
   it. A shadow could not be commanded.
2. **The device is an actuator and a source.** `fixtureDevice` simulates the PLC: it reports
   `state` and accepts commands. In production this is your own `Actuator`, sending the command
   to the real controller, plus the source that reports its state.
3. **The policy decides.** Operators have every Action in the `site` World; visitors have
   none. Swap in your own roles and Worlds; see [Give a team its own view](https://docs.klorad.com/guides/entitled-views).
4. **The Action carries a command.** `open` applies to twins and sends `{ name: "open" }`. Its
   Behaviour only records that the opening was requested.
5. **The reading closes the loop.** About 600 ms later the device reports `state: "open"`, and
   `scene.latest("barrier", "state")` follows.

## Handling outcomes

Show the outcome to the person who acted. `denied` and `failed` carry a `reason` worth
displaying; `sent` means "the device accepted it", not "it happened". If the device never
reports back, the twin keeps showing the old state, which is the truth as far as anyone knows.

---

# Raise alerts

Source: https://docs.klorad.com/guides/alerts

Rules over readings and Actions raise alerts, delivered only to the Worlds whose scope contains the object.

`@klorad/notify` watches a scene's events and raises an alert when a rule's condition becomes
true. Alerts are addressed to Worlds, and an alert reaches a World only if the object is in that
World's scope, so a crew is never alerted about what it cannot see.

```ts title="alerts.ts"
import { createAccessPolicy } from "@klorad/access";
import { createScene } from "@klorad/api/world";
import { createNotifier, inAppChannel } from "@klorad/notify";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({
  id: "door-3",
  name: "Loading door 3",
  position: { east: 40, north: 10 },
  correspondence: "shadow",
  binding: { source: "acs", entity: "door-3" },
  properties: { zone: "dock" },
});

const policy = createAccessPolicy({
  scene,
  roles: { guard: "*" },
  worlds: [{ id: "control", name: "Control room", scope: { all: true }, capabilities: { actions: "*" }, principals: [{ kind: "role", id: "guard" }] }],
});

const inbox = inAppChannel();
const notifier = createNotifier({
  scene,
  policy,
  channels: [inbox],
  rules: [
    { id: "forced", name: "Door forced", condition: { kind: "equals", quantity: "state", value: "forced" }, severity: "critical", worlds: ["control"] },
    {
      id: "warm",
      name: "Dock too warm",
      condition: { kind: "threshold", quantity: "temperature", op: "gt", value: 30 },
      objects: { properties: { zone: "dock" } },
      severity: "warning",
      worlds: ["control"],
    },
  ],
});

scene.observe({ objectId: "door-3", quantity: "state", value: "forced", observedAt: Date.now(), source: "acs" });

const [alert] = inbox.list(["control"]); // "Door forced: Loading door 3 state is forced"
if (alert) notifier.acknowledge(alert.id, { id: "maria", roles: ["guard"] });

export { notifier };
```

## Conditions

| Kind | Fires when |
| --- | --- |
| `threshold` | A numeric reading crosses a limit (`gt`, `gte`, `lt`, `lte`). |
| `equals` | A reading equals a value: a door `state` of `"forced"`, a pump `running` of `false`. |
| `action` | An Action ends with one of the given outcomes, for example every `denied` request. |

Threshold and equality rules fire on the **edge**: once when the condition becomes true for an
object, and again only after it has stopped holding. A temperature hovering above the limit
raises one alert, not one per reading. A late reading never fires a rule, because it does not
change the current state.

`objects` narrows a rule to some objects, by id or by properties.

## Channels

A channel delivers alerts. `inAppChannel()` keeps them in memory for a notification centre: it
lists alerts per World and can be subscribed to (it works with React's
`useSyncExternalStore`). A channel is one `deliver(alert)` method, so web push, email or a
message queue are small adapters in your server code. One failing channel does not stop the
others.

## Acknowledging

`notifier.acknowledge(alertId, actor)` marks an alert acknowledged, and only for an actor who
may enter one of the alert's Worlds.

---

# Connect a polling API

Source: https://docs.klorad.com/guides/connect-a-polling-api

Read a vendor endpoint on an interval, validate what comes back, and route it to your shadows.

Many systems you will meet, from traffic management to building management, offer only a pull
API: you ask for the current state, they answer. `pollingSource` turns such an API into a
source of readings.

```ts title="polling-api.ts"
import { z } from "zod";
import { connectSource, pollingSource } from "@klorad/api/integration";
import { createScene } from "@klorad/api/world";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({
  id: "vms-12",
  name: "Sign VMS-12",
  position: { east: 120, north: 40 },
  correspondence: "shadow",
  binding: { source: "atms", entity: "vms-12" },
});

// What the vendor promises to send. Anything else is refused before it reaches the scene.
const SignsResponse = z.object({
  signs: z.array(z.object({ id: z.string(), message: z.string(), updatedAt: z.string().datetime() })),
});

const atms = pollingSource({
  id: "atms",
  intervalMs: 15_000,
  read: async () => {
    const response = await fetch("https://atms.example.com/api/signs");
    const body = SignsResponse.parse(await response.json());
    return body.signs.map((s) => ({ entity: s.id, quantity: "message", value: s.message, observedAt: Date.parse(s.updatedAt) }));
  },
  onError: (error) => console.warn("ATMS poll failed", error),
});

const connection = connectSource(scene, atms);

// Counts every reading: received, latest, late, duplicate, unbound and rejected.
const { received, unbound } = connection.stats();

export { connection, received, unbound };
```

## What matters

**Bind by the vendor's own ids.** The source reports `entity: s.id` exactly as the vendor
names it, and each shadow's `binding.entity` uses the same id. Mapping happens in bindings,
not in the source.

**Use the vendor's timestamp.** `observedAt` must be when the vendor observed the state, not
when you polled. A poll that returns an unchanged sign with an unchanged `updatedAt` then
becomes a `duplicate` and costs nothing, and an out of date answer cannot overwrite a newer one.

**Validate at the boundary.** The schema runs before anything reaches the scene. If the vendor
changes its payload, `parse` throws, the poll fails, `onError` hears about it, and the twin
keeps its last good state instead of filling with nonsense.

**Keep credentials on the server.** If the API needs a key, run the polling source in server
code (a route handler, a worker) and forward readings to browsers; never ship the key to the
client.

## When something does not show up

Look at `connection.stats()`. A growing `unbound` count means readings arrive for entities no
object is bound to: compare the vendor's ids with your bindings. A growing `duplicate` count
is normal for polling.

---

# Give a team its own view

Source: https://docs.klorad.com/guides/entitled-views

A district crew sees and acts on its own bins only, the city sees everything, and every Action is audited.

One twin of a city's waste bins, two audiences: the city's operations room sees every bin, and
the north district crew sees and acts on the north district only. You do not copy the twin;
you define two Worlds over it.

```ts title="worlds.ts"
import { auditActions, createAccessPolicy, createMemoryAuditLog, type World } from "@klorad/access";
import { createScene } from "@klorad/api/world";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({ id: "n1", name: "Bin N1", position: { east: -30, north: 70 }, correspondence: "shadow", binding: { source: "bins", entity: "n1" }, properties: { district: "north" } });
scene.add({ id: "s1", name: "Bin S1", position: { east: -25, north: -80 }, correspondence: "shadow", binding: { source: "bins", entity: "s1" }, properties: { district: "south" } });

const worlds: World[] = [
  { id: "city", name: "City operations", scope: { all: true }, capabilities: { actions: "*" }, principals: [{ kind: "role", id: "admin" }] },
  {
    id: "north",
    name: "North crew",
    scope: { properties: { district: "north" } },
    capabilities: { actions: ["mark-emptied"] },
    principals: [{ kind: "team", id: "crew-north" }],
    presentation: { primary: "#0e7490", visibility: "authenticated" },
  },
];

const audit = createMemoryAuditLog();
const policy = createAccessPolicy({ scene, roles: { admin: "*", crew: ["mark-emptied"] }, worlds, audit });
auditActions(scene, audit);

const kostas = { id: "kostas", roles: ["crew"], teams: ["crew-north"] };
const entered = policy.worldsFor(kostas).map((w) => w.name); // ["North crew"]

// What Kostas receives: a scene with Bin N1 only, kept in step with the twin.
const view = policy.view("north", kostas);
const visible = view.scene.objects().map((o) => o.id); // ["n1"]

export { entered, visible };
```

## Designing Worlds

**Scope by properties, not by lists.** `scope: { properties: { district: "north" } }` keeps
working as bins are added; a list of ids needs maintenance. Use `within` (a centre and a
radius) for geographic areas, and `correspondence` to show, say, only shadows.

**Give capabilities, not roles, to Worlds.** Roles say what a person may do anywhere in the
organisation; a World's capabilities say what may be done in that World. The person needs
both, so the crew role can be broad while the World stays narrow.

**Principals can be users, teams or roles.** Prefer teams and roles: people change jobs, and
a World that lists individuals goes stale.

**Hand members the view, never the twin.** `policy.view(worldId, actor)` throws if the actor
may not enter, and returns a scene with in scope objects only. Build the member's interface on
that scene, and objects outside the World never reach their browser.

## Auditing

`auditActions(scene, audit)` records every Action outcome, including denials, and the policy
records World changes made with `setWorld` and `removeWorld`. In a server app, implement
`AuditSink` over an append only table.

---

# Guides

Source: https://docs.klorad.com/guides

One task per page. Each guide assumes you have done the quickstart and shows the smallest code that does the job.

- [Connect a polling API](https://docs.klorad.com/guides/connect-a-polling-api): Read a vendor endpoint on an interval, validate the payload, route it to shadows.
  - [Style objects by readings](https://docs.klorad.com/guides/style-objects-by-readings): Colour and scale objects by their state, and show when a sensor goes quiet.
  - [Act on a twin](https://docs.klorad.com/guides/act-on-a-twin): Define an Action that sends a command to a device, and see the twin follow.
  - [Give a team its own view](https://docs.klorad.com/guides/entitled-views): Worlds: a crew sees and acts on its district only; everything is audited.
  - [Raise alerts](https://docs.klorad.com/guides/alerts): Rules over readings and Actions, delivered to the Worlds that may see them.
  - [Save and restore a scene](https://docs.klorad.com/guides/save-and-restore): Snapshot a scene to JSON and rebuild it elsewhere.
  - [Test with a manual clock](https://docs.klorad.com/guides/test-with-a-manual-clock): Deterministic tests of staleness and ordering with node:test.

---

# Save and restore a scene

Source: https://docs.klorad.com/guides/save-and-restore

Snapshot a scene to JSON, with its objects and readings, and rebuild it elsewhere.

`snapshotScene` turns a scene into a plain, versioned JSON value: its origin, its objects, the
history of their readings and the staleness window. `restoreScene` builds a new scene from it.

```ts title="snapshot.ts"
import { createScene, restoreScene, snapshotScene, type SceneSnapshot } from "@klorad/api/world";

const scene = createScene({ name: "Waterfront", coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({ id: "hall", name: "Exhibition hall", position: { east: 0, north: 0 }, geometry: { kind: "box", width: 24, depth: 14, height: 7 } });

// Plain JSON: objects, their observations, the origin and the staleness window.
const json = JSON.stringify(snapshotScene(scene));

// Later, or on another machine: the same scene, with a fresh event stream.
const restored = restoreScene(JSON.parse(json) as SceneSnapshot);
restored.get("hall")?.name; // "Exhibition hall"

export { restored };
```

## What is and is not in a snapshot

| In | Not in |
| --- | --- |
| Id, name, origin, `staleAfterMs` | Subscribers and the event stream (a restored scene starts a new one) |
| Every object, with its correspondence and binding | Connections to sources (connect them again) |
| Every observation kept in history | Interaction definitions and Access policies (they are code) |

The snapshot carries `format: "klorad.scene"` and a `version`, so a future format can be read
alongside this one. Pass a `clock` or other options as the second argument of `restoreScene`
when the restored scene needs them.

## When to use it

Fixtures for tests and demos, a "start from here" state for a new environment, and exporting a
twin to another tool. It is not a live replication mechanism: for that, persist operator
decisions in your database and keep live state at its source.

---

# Style objects by readings

Source: https://docs.klorad.com/guides/style-objects-by-readings

Colour and scale objects by their current state, and show when a sensor has gone quiet.

The default look encodes correspondence and freshness. To show your own state, give the
renderer an `objectStyle` function. It runs for every object whenever the object, its readings
or its staleness change.

```ts title="style-by-readings.ts"
import type { StyleFn } from "@klorad/engine-three";

/** Colours a room by how full it is, and greys it out when its counter goes quiet. */
export const occupancyStyle: StyleFn = ({ object, readings, stale, palette }) => {
  if (stale) return { color: palette.stale, opacity: 0.6 };
  const people = readings.occupancy?.value;
  if (typeof people !== "number") return { color: object.color ?? palette.object };
  const load = people / Number(object.properties.capacity ?? 50);
  const color = load > 0.9 ? "#dc2626" : load > 0.6 ? "#f59e0b" : palette.shadow;
  return { color, heightScale: Math.max(0.2, Math.min(load, 1.5)) };
};
```

The function receives:

<TypeTable
  type={{
    object: { type: "SceneObject", description: "The object, with its properties." },
    readings: { type: "Record<string, Observation>", description: "The current reading of every quantity, by name." },
    stale: { type: "boolean", description: "True when the object is a shadow or twin and all its readings are older than its staleness window, or it has none." },
    palette: { type: "Palette", description: "The renderer's colours, so your style matches the rest of the view." },
  }}
/>

and returns any of `color`, `opacity` and `heightScale`. Anything it leaves out keeps the
default.

## In React

```tsx title="rooms-view.tsx"
"use client";

import { ThreeView } from "@klorad/engine-three/react";
import { occupancyStyle } from "./style-by-readings";

// Options are read when the view mounts: change `key` to apply a different style.
export function RoomsView() {
  return <ThreeView objectStyle={occupancyStyle} palette={{ background: "#0b1220" }} />;
}
```

`ThreeView` reads its options when it mounts. To switch styles at runtime, change its `key`.

## Keep it pure

A style function is called often and must not change anything: no scene writes, no fetches. If
a colour depends on something outside the scene, put that something in the scene first (as a
property or a reading), so every view and every hook agrees on it.

---

# Test with a manual clock

Source: https://docs.klorad.com/guides/test-with-a-manual-clock

Deterministic tests of staleness and ordering, with node:test and createManualClock.

Time-dependent behaviour is the easiest to get wrong and the hardest to test with real time.
Give the scene a manual clock and you decide exactly when "now" is.

```ts title="stale.test.ts"
import assert from "node:assert/strict";
import { test } from "node:test";
import { createManualClock, createScene } from "@klorad/api/world";

test("a quiet sensor turns stale after its window", () => {
  const clock = createManualClock(Date.parse("2026-10-09T10:00:00Z"));
  const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } }, clock, staleAfterMs: 60_000 });
  scene.add({ id: "t1", name: "Probe", position: { east: 0, north: 0 }, correspondence: "shadow", binding: { source: "bms", entity: "t1" } });

  scene.observe({ objectId: "t1", quantity: "temperature", value: 21.5, observedAt: clock.now(), source: "bms" });
  assert.equal(scene.isStale("t1", "temperature"), false);

  clock.advance(61_000);
  assert.equal(scene.isStale("t1", "temperature"), true);
});
```

Run it with Node's built in test runner (`node --test`, through `tsx` for TypeScript). The same
clock can be passed to `fixtureSource` and `fixtureDevice`, so the readings they produce are
stamped with time you control (their intervals still run on real timers).

## What to test this way

- A reading observed earlier than the current one is `late` and does not change `latest`.
- A sensor turns stale exactly after its window, and fresh again with the next reading.
- A rule in `@klorad/notify` fires once on the edge, not on every reading above a limit.
- A Behaviour stamps `now` from the scene's clock, so its output is predictable.

---

# Introduction

Source: https://docs.klorad.com/get-started

What Klorad is, what you can build with it today, and how these docs are organised.

Klorad is a TypeScript SDK for building digital twins: software models of real places and
things that stay in step with them. You describe a scene anchored on Earth, put objects in it,
connect the sensors and systems that report on those objects, and decide who may see and act
on what. A renderer draws the result; React bindings let you build the interface around it.

## What you can build today

With version 0.1 you can:

- anchor a scene on a real place and position objects by latitude and longitude or by metres
  from an origin;
- classify every object as a **Digital Object**, a **Digital Shadow** (fed by readings) or a
  **Digital Twin** (fed by readings and able to receive commands);
- connect sources of readings: generated fixtures, any API you can poll, and simulated devices
  that accept commands;
- order readings by when they were observed, so late and duplicate data never corrupt the
  current state, and tell when a sensor has gone quiet;
- define **Actions** that only entitled people can take, the **Behaviours** that follow and the
  **Animations** that show them;
- give each team a **World**: a view of the twin that contains only the objects and Actions that
  team is entitled to, with every Action written to an audit log;
- raise alerts from rules over the scene's events;
- draw all of it with three.js, in plain TypeScript or as a React component.

The [sandbox](https://docs.klorad.com/sandbox) runs each of these live.

  A realtime transport between browsers, connectors for named vendors and renderers beyond
  three.js are on the roadmap, not in 0.1. These pages describe only what the
  packages do today.

## How these docs are organised

  - [Get started](https://docs.klorad.com/get-started/quickstart): Install the packages and build a working twin, step by step.
  - [Concepts](https://docs.klorad.com/concepts): The model behind the API, one idea per page. Read these to understand why.
  - [Guides](https://docs.klorad.com/guides): Short recipes for one task each: connect an API, add an Action, give a team a World.
  - [Reference](https://docs.klorad.com/reference): Every package and export: what it takes, what it returns, what it guarantees.

If you are new, follow the [quickstart](https://docs.klorad.com/get-started/quickstart) first, then read
[the model](https://docs.klorad.com/concepts/model). If you already know what you want to do, go to the guide for it.

## Who it is for

Developers at organisations that build or run digital twins with Klorad: Prieston's customers
and partners, and research groups with research access. The SDK is licensed software;
[Install](https://docs.klorad.com/get-started/install) explains how access works. You need to know TypeScript; React
is optional. The docs are written for you and for the AI agents you work with.

---

# Install

Source: https://docs.klorad.com/get-started/install

How access to the Klorad packages works, which ones your app needs, and how to install them locally and in CI.

The Klorad SDK is licensed software. The packages are private packages on npm under the
`@klorad` scope, available to developers at organisations that hold a licence agreement with
Prieston Technologies: customers, partners, and research groups with research access.

## Getting access

Prieston Technologies adds the npm account of each developer covered by your licence to a read
only team of the `@klorad` organisation on npm. Send your contact at Prieston the npm user
names of the developers who need access. Nothing else is installed or configured on your side.

## Requirements

- Node.js 18 or newer
- pnpm or npm
- an npm account with access, as above

## On your machine

```sh
npm login
pnpm add @klorad/api @klorad/react @klorad/engine-three three
```

`npm login` stores your credentials in your user `.npmrc`, and pnpm reads them from there too.

## In CI

Create a granular access token on npmjs.com with read only access to the `@klorad` packages,
store it as a secret (for example `NPM_TOKEN`), and add an `.npmrc` to the project:

```ini
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
```

The token stays in the CI secret store; the `.npmrc` only refers to it.

## Which packages you need

| Package | You need it when | Peer dependencies |
| --- | --- | --- |
| `@klorad/api` | Always. The world model (`/world`) and the integration layer (`/integration`). | none |
| `@klorad/react` | You build the interface in React. | `react` 18.3 or 19 |
| `@klorad/engine-three` | You draw the scene in 3D. `/react` adds the `ThreeView` component. | `three` 0.170 or newer; `react` and `@klorad/react` for `/react` |
| `@klorad/access` | More than one kind of user: Worlds, entitlement, audit log. | none |
| `@klorad/notify` | Alerts from rules over readings and Actions. | none |

Each package is ESM only, ships its own type declarations, and has no runtime dependency
outside the Klorad packages it names. A renderer is chosen by installing it: an app that does
not draw in 3D never downloads three.js.

## Licence

The packages are proprietary. You may use them only under your licence agreement with
Prieston Technologies and within the scope it grants; the `LICENSE` file in each package says
the same. Builds are minified; the type declarations are complete and readable, and they are
what you and your agents code against.

---

# Quickstart

Source: https://docs.klorad.com/get-started/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.

  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](https://docs.klorad.com/get-started/install) explains how access works.

### Sign in to npm

```sh
npm login
npm view @klorad/api version
```

If 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`:

```json title="my-twin/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`:

```tsx title="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.

```tsx title="my-twin/app/twin.tsx"
"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 } } }}>

      <div style={{ height: 480 }}>

      </div>
    </KloradProvider>
  );
}
```

Three things to notice:

- **Position is real.** `east: 16, north: -10` means 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 `binding` names a source and an entity, so readings
  from `gateway` about `entrance-01` land on it and nowhere else.
- **`useLatest` re-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`:

```tsx title="my-twin/app/page.tsx"
import Twin from "./twin";

export default function Page() {
  return (
    <main style={{ padding: 24 }}>
      <h1>My first twin</h1>

    </main>
  );
}
```

### Run it

From the `my-twin` folder:

```sh
pnpm install
pnpm dev
```

Open [http://localhost:3100](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.

## Next

  - [Replace the fixture with a real API](https://docs.klorad.com/guides/connect-a-polling-api): Poll a vendor endpoint, validate what comes back, and route it to your shadows.
  - [Colour objects by their readings](https://docs.klorad.com/guides/style-objects-by-readings): Map state to colour and height with objectStyle.
  - [Understand the model](https://docs.klorad.com/concepts/model): Three layers, four pillars, and why the API is shaped by them.
  - [Let people act on the twin](https://docs.klorad.com/guides/act-on-a-twin): Actions, Entitlement, Behaviours, and commands to a device.

---

# @klorad/access

Source: https://docs.klorad.com/reference/access

The Access layer. Worlds, the Entitlement the World layer asks, projections per World and the audit log.

```ts
import { createAccessPolicy, createMemoryAuditLog, auditActions } from "@klorad/access";
```

| Export | Kind | Description |
| --- | --- | --- |
| `createAccessPolicy(options)` | function | Returns an `AccessPolicy`: the `Entitlement` for `createInteraction`, plus Worlds and views. |
| `AccessPolicy` | type | `check`, `world(id)`, `worldsFor(actor)`, `setWorld(world, by)`, `removeWorld(id, by)`, `view(worldId, actor)`. |
| `RolePolicy` | type | Organisation roles to the Action ids they allow, or `"*"`. |
| `allowEveryone` | value | An Entitlement that allows everything. For demos and single user tools; the name says so. |
| `World`, `Scope`, `Capabilities`, `Principal`, `Presentation` | type | An entitled view: see [Worlds](https://docs.klorad.com/concepts/worlds). |
| `assertWorld(world)` | function | Validates a World and returns it; throws with the reason. |
| `inScope(cs, object, scope)` | function | Whether an object is inside a scope. |
| `canEnter(world, actor)` | function | Whether an actor is one of a World's principals. |
| `createMemoryAuditLog()` | function | An in memory, append only `AuditSink` with `entries()`. |
| `auditActions(scene, sink)` | function | Writes every Action outcome to the sink. Returns a stop function. |
| `AuditSink`, `AuditEntry`, `MemoryAuditLog` | type | The audit port and its entries. |

## Scope

| Form | Contains |
| --- | --- |
| `{ all: true }` | Every object. |
| `{ objects: [...] }` | The listed ids, whatever else is given. |
| `{ correspondence, properties, within }` | Objects matching every criterion given: kinds, property values, and `within: { center, radiusMeters }`. |

---

# @klorad/api/integration

Source: https://docs.klorad.com/reference/api-integration

The Integration layer. Sources of readings, the connection that routes them, and a simulated device.

```ts
import { connectSource, fixtureSource, pollingSource, fixtureDevice } from "@klorad/api/integration";
```

| Export | Kind | Description |
| --- | --- | --- |
| `ObservationSource` | type | `{ id, start(sink) }`. `start` begins reporting and returns a stop function. |
| `SourceReading`, `ReadingSink` | type | A reading about the source's own entity: `entity`, `quantity`, `value`, `unit`, `observedAt`, `seq`, `quality`. |
| `connectSource(scene, source)` | function | Starts the source and routes each reading to the objects bound to it. Returns a `Connection`. |
| `Connection`, `ConnectionStats` | type | `source`, `stats()`, `stop()`. Stats count `received`, `latest`, `late`, `duplicate`, `unbound`, `rejected`. |
| `fixtureSource(options)` | function | Generated readings on an interval. A demo, not a device. |
| `pollingSource(options)` | function | Calls `read()` on an interval; never overlaps a slow call. |
| `fixtureDevice(options)` | function | A simulated device: an `ObservationSource` and an `Actuator` in one. |

## fixtureSource

`lagMs` makes a reading observed earlier than it is reported; a lag longer than the interval
arrives out of order, which is how the sandbox shows late readings.

## pollingSource

<TypeTable
  type={{
    id: { type: "string", required: true },
    read: { type: "() => Promise<SourceReading[]>", required: true, description: "The readings available now. Validate vendor payloads here." },
    intervalMs: { type: "number", default: "15000" },
    onError: { type: "(error: unknown) => void", description: "Hears a failed read; polling continues." },
  }}
/>

## fixtureDevice

<TypeTable
  type={{
    id: { type: "string", required: true, description: "Its source id, and the source its actuator serves." },
    entities: { type: "Record<string, DeviceState>", required: true, description: "Initial state per entity: quantity to value." },
    respond: { type: "(command, state) => DeviceState | string", required: true, description: "The new state after a command, or a string to refuse it with that reason." },
    latencyMs: { type: "number", default: "600", description: "Between accepting a command and reporting the new state." },
    clock: { type: "Clock", default: "systemClock" },
  }}
/>

---

# @klorad/api/world

Source: https://docs.klorad.com/reference/api-world

The World layer. Scenes, Scene Objects, space, time, snapshots, interaction and projections. Pure TypeScript.

```ts
import { createScene } from "@klorad/api/world";
```

## Scene

| Export | Kind | Description |
| --- | --- | --- |
| `createScene(options)` | function | Creates an empty scene anchored on Earth. Throws without a `coordinateSystem`. |
| `Scene` | type | The scene: the only way to change world state. |
| `SceneOptions` | type | Options of `createScene`. |
| `SceneEvent`, `SceneListener` | type | The ordered events every change becomes. |
| `snapshotScene(scene)`, `restoreScene(snapshot, options?)` | function | To and from a JSON `SceneSnapshot`. |
| `projectScene(source, admits, name?)` | function | A read model holding only the objects `admits` accepts, kept in step. Returns a `Projection` (`scene`, `stop()`). |

<TypeTable
  type={{
    coordinateSystem: { type: "CoordinateSystem | { origin: GeoPoint }", description: "The Earth anchor. Required.", required: true },
    id: { type: "string", default: '"scene"' },
    name: { type: "string", default: '"Untitled scene"' },
    clock: { type: "Clock", default: "systemClock", description: "Where the scene reads now from." },
    staleAfterMs: { type: "number", default: "300000", description: "A reading older than this is stale unless its object sets its own." },
    historyLimit: { type: "number", default: "500", description: "Observations kept per object and quantity." },
    onListenerError: { type: "(error: unknown) => void", description: "Hears about subscribers that throw; the change stands either way." },
  }}
/>

### Scene methods

| Method | Returns | Description |
| --- | --- | --- |
| `add(spec)` | the object, typed by its spec | Adds a Scene Object. Throws on a duplicate id or an invalid spec. |
| `update(id, patch)` | `SceneObject` | Patches name, position, orientation, scale, geometry, colour or properties (merged). Throws on an unknown id. |
| `remove(id)` | `boolean` | Removes an object and its readings. |
| `get(id)` | `SceneObject \| undefined` | |
| `objects()` | `readonly SceneObject[]` | In insertion order; the same array until something changes. |
| `observe(input)` | `ObserveResult` | Offers a reading: `latest`, `late`, `duplicate` or `rejected`. |
| `latest(id, quantity)` | `Observation \| undefined` | The current reading. |
| `readings(id)` | `Record<string, Observation>` | The current reading of every quantity; the same object until one changes. |
| `history(id, quantity)` | `readonly Observation[]` | Ordered by `observedAt`, then `seq`. |
| `isStale(id, quantity)` | `boolean` | Against the scene's clock, now. |
| `boundTo(source, entity)` | `readonly SceneObject[]` | The shadows and twins bound to a source entity. |
| `subscribe(listener)` | `() => void` | Every event, in order. Returns an unsubscribe function. |
| `version()` | `number` | The `seq` of the last event. |

## Scene Objects

| Export | Kind | Description |
| --- | --- | --- |
| `SceneObject` | type | `DigitalObject \| DigitalShadow \| DigitalTwin`, discriminated by `correspondence`. |
| `SceneObjectSpec`, `DigitalObjectSpec`, `DigitalShadowSpec`, `DigitalTwinSpec` | type | What `add` takes. |
| `SceneObjectPatch` | type | What `update` takes. |
| `Correspondence` | type | `"object" \| "shadow" \| "twin"`. |
| `SourceBinding` | type | `{ source, entity }`. |
| `Geometry` | type | `box`, `cylinder`, `sphere`, `marker` or `model` (a glTF `uri`). |
| `Orientation` | type | `heading`, `pitch`, `roll` in degrees. |
| `isObservable(object)` | function | Narrows to a shadow or a twin. |

## Space

| Export | Kind | Description |
| --- | --- | --- |
| `createCoordinateSystem({ origin })` | function | An East, North, Up frame in metres at a WGS84 origin: `toLocal`, `toGeo`, `resolve`. |
| `Position`, `GeoPosition`, `LocalPosition`, `GeoPoint`, `LocalPoint` | type | A position on Earth (`lat`, `lon`, `height`) or in metres (`east`, `north`, `up`). |
| `isLocalPosition(p)` | function | True for the local form. |
| `geoPoint`, `geodeticToEcef`, `ecefToGeodetic`, `toRadians`, `toDegrees` | function | Geodesy helpers on WGS84. |

## Time

| Export | Kind | Description |
| --- | --- | --- |
| `Clock`, `Instant` | type | `now()` in epoch milliseconds. |
| `systemClock` | value | The default clock. |
| `createManualClock(start)` | function | A clock you `advance(ms)` or `set(instant)`, for tests. |
| `Observation`, `ObservationInput`, `ObservationValue`, `ObservationQuality`, `ObserveResult` | type | A reading: `objectId`, `quantity`, `value`, `unit`, `observedAt`, `receivedAt`, `source`, `quality`, `seq`. |
| `compareObservations(a, b)` | function | The Time Spectrum order: `observedAt`, then `seq`. |

## Interaction

| Export | Kind | Description |
| --- | --- | --- |
| `createInteraction(scene, options)` | function | Runs Action, Entitlement, Behaviour, Animation. Returns an `Interaction`. |
| `Interaction` | type | `available(objectId, actor, world?)` and `perform(request)`. |
| `ActionDefinition`, `BehaviourDefinition`, `BehaviourContext`, `AnimationRequest` | type | What you define. |
| `ActionRequest`, `ActionResult`, `ActionOutcome`, `ActionParams`, `ParamValue`, `Actor` | type | What you ask and what you get back. |
| `Entitlement`, `Decision` | type | The port Access implements. |
| `Actuator`, `Command`, `CommandReceipt` | type | The port toward devices, for Digital Twins. |

---

# @klorad/engine-three

Source: https://docs.klorad.com/reference/engine-three

Draws a scene with three.js, follows its events, and never changes world state.

```ts
import { createThreeRenderer } from "@klorad/engine-three";
import { ThreeView } from "@klorad/engine-three/react";
```

| Export | Kind | Description |
| --- | --- | --- |
| `createThreeRenderer(options)` | function | Draws the scene into `container`. Returns a `ThreeRenderer`: `canvas`, `select(id)`, `frame(ids?)`, `dispose()`. |
| `ThreeView` (`/react`) | component | The renderer as a React component; reads the scene from `KloradProvider` unless given one. |
| `defaultPalette`, `defaultStyle` | value | The default colours and style function. |
| `Palette`, `ObjectStyle`, `StyleContext`, `StyleFn` | type | For `objectStyle`. |
| `localToThree`, `threeToLocal`, `geoToThree`, `orientationToEuler` | function | Frame conversions: east is +x, up is +y, north is -z. |

<TypeTable
  type={{
    container: { type: "HTMLElement", required: true, description: "Not on ThreeView, which renders its own." },
    scene: { type: "Scene", required: true, description: "Optional on ThreeView." },
    palette: { type: "Partial<Palette>", description: "background, ground, grid, object, shadow, twin, stale, selected." },
    objectStyle: { type: "StyleFn", default: "defaultStyle", description: "Maps state to color, opacity and heightScale." },
    gridSize: { type: "number", description: "Size of the ground grid in metres." },
    camera: { type: "{ position?, target? }", description: "Initial camera, in local metres." },
    onPick: { type: "(objectId: string | null) => void", description: "A click on an object, or on nothing." },
    onError: { type: "(error: unknown) => void", description: "For example a model that fails to load." },
  }}
/>

`ThreeView` adds `selected`, `autoFrame` (default `true`), `className` and `style`. It reads its
options when it mounts; change its `key` to apply new ones.

---

# For agents

Source: https://docs.klorad.com/reference/for-agents

How a coding agent should read these docs and work with the SDK.

These docs are written to be read by people and by coding agents. If you are an agent:

- Fetch [`/llms.txt`](https://docs.klorad.com/llms.txt) for an index of every page, or
  [`/llms-full.txt`](https://docs.klorad.com/llms-full.txt) for all of them as one Markdown file.
- Every code sample on this site is a file that compiles against the current packages. Copy
  from the samples rather than inventing calls.

## Rules the SDK holds you to

- Change world state only through a `Scene`. A renderer or a component that needs to change the
  world calls back to app code, which calls the scene.
- `@klorad/api/world` imports nothing but itself: no React, no three.js, no DOM, no Node APIs.
  Code that needs those belongs in a binding, a renderer or Integration.
- Give every shadow and twin a `binding`, and give a binding only to a shadow or a twin.
- Order comes from `observedAt`. Never use arrival time as the state of the world.
- A Behaviour cannot request an Action. Model follow up work as a new request that the
  Entitlement checks again.
- Validate outside data with a schema at the source boundary; keep vendor credentials on the
  server.

## Where the truth is

The type declarations in `node_modules/@klorad/<name>/dist/*.d.ts` are the reference: they are
complete, readable and match the installed version, while the JavaScript next to them is
minified. When this site and the declarations disagree, the declarations win; tell your
contact at Prieston so the page is fixed.

---

# Reference

Source: https://docs.klorad.com/reference

Every public package and export of the Klorad SDK, what it takes and what it returns.

| Package | Import | Layer | Page |
| --- | --- | --- | --- |
| `@klorad/api` | `@klorad/api/world` | World | [World](https://docs.klorad.com/reference/api-world) |
| `@klorad/api` | `@klorad/api/integration` | Integration | [Integration](https://docs.klorad.com/reference/api-integration) |
| `@klorad/access` | `@klorad/access` | Access | [Access](https://docs.klorad.com/reference/access) |
| `@klorad/notify` | `@klorad/notify` | Platform service over Access | [Notify](https://docs.klorad.com/reference/notify) |
| `@klorad/react` | `@klorad/react` | Binding | [React](https://docs.klorad.com/reference/react) |
| `@klorad/engine-three` | `@klorad/engine-three`, `/react` | Renderer | [three.js renderer](https://docs.klorad.com/reference/engine-three) |

`@klorad/api` also exports everything from `/world` and `/integration` at its root; prefer the
subpaths, which say which layer your code depends on.

## Stability

The packages are at 0.x. Under [semantic versioning](https://docs.klorad.com/reference/releases), a 0.x minor release
may change the public API; every such change is listed in the package's changelog with a
migration line. Exports not documented here are internal, even when a bundler lets you reach
them.

## Conventions

- Every value the SDK returns is immutable (frozen) unless its type says otherwise.
- Functions that can fail on bad input throw a `TypeError` or `RangeError` with a message that
  names the fix. Operations that can fail on the outside world (a device, a vendor) return an
  outcome instead of throwing.
- Instants are milliseconds since the Unix epoch (`Instant = number`), always on the scene's
  clock.

---

# @klorad/notify

Source: https://docs.klorad.com/reference/notify

Rules over a scene's events raise alerts, addressed to Worlds and never wider than their scope.

```ts
import { createNotifier, inAppChannel } from "@klorad/notify";
```

| Export | Kind | Description |
| --- | --- | --- |
| `createNotifier(options)` | function | Watches the scene and raises alerts. Returns a `Notifier`: `alerts()`, `acknowledge(id, actor)`, `stop()`. |
| `Rule`, `Condition`, `ObjectFilter`, `Severity` | type | A rule: `id`, `name`, `condition`, `objects?`, `severity` (`info`, `warning`, `critical`), `worlds`. |
| `Alert` | type | `id`, `ruleId`, `objectId`, `severity`, `title`, `at`, `worlds`, and once acknowledged `acknowledgedBy`, `acknowledgedAt`. |
| `Channel` | type | `{ id, deliver(alert) }`. |
| `inAppChannel(limit?)` | function | An in memory channel: `list(worlds)`, `subscribe(listener)`, `update(alert)`. Keeps the last 200 by default. |
| `holds(condition, value)`, `watches(rule, object)` | function | The rule predicates, for your own tooling. |

<TypeTable
  type={{
    scene: { type: "Scene", required: true },
    policy: { type: "AccessPolicy", required: true, description: "Resolves each rule's Worlds and their scopes." },
    rules: { type: "Rule[]", required: true },
    channels: { type: "Channel[]", required: true },
    onError: { type: "(error: unknown) => void", description: "Hears a failing channel; the others still deliver." },
  }}
/>

Not in 0.1: web push, email and SMS channels, escalation, quiet hours and persisted alerts.

---

# @klorad/react

Source: https://docs.klorad.com/reference/react

React bindings. Components declare what exists; hooks read and subscribe. No world logic.

```tsx
"use client";
import { KloradProvider, SceneObject, Connector, useLatest } from "@klorad/react";
```

The package is a client module (it ships with `"use client"`). Hooks use
`useSyncExternalStore` over the scene, so they re-render only when what they read changes.

## Components

| Export | Description |
| --- | --- |
| `KloradProvider` | Creates a scene from `options` (the same as `createScene`), or provides the `scene` you pass. |
| `SceneObject` | Declares an object: added on mount, updated when its props change, removed on unmount. Takes the props of a `SceneObjectSpec` and needs an `id`. Changing its id, correspondence or binding replaces the object instead of updating it. |
| `Connector` | Connects a source to the scene while mounted. `useConnector(source)` is the hook form. |
| `InteractionProvider` | Provides an `Interaction` to the hooks below. |

## Hooks

| Hook | Returns |
| --- | --- |
| `useScene()` | The scene from the nearest provider. |
| `useSceneObjects()` | Every object. |
| `useSceneObject(id)` | One object, or `undefined`. |
| `useReadings(id)` | The current reading of every quantity of an object. |
| `useLatest(id, quantity)` | The current reading of one quantity. |
| `useShadow(id)` | `{ object, readings, isStale(quantity) }`. |
| `useSceneVersion()` | The scene's event sequence number. |
| `useInteraction()` | The `Interaction` from `InteractionProvider`. |
| `useActions(objectId, actor, world?)` | The Actions this actor may take on the object now. |
| `useAction()` | `[perform, { pending, result }]`. |

## Rendering

The 3D view is `ThreeView` from [`@klorad/engine-three/react`](https://docs.klorad.com/reference/engine-three), which
reads the scene from `KloradProvider`.

---

# Versions and releases

Source: https://docs.klorad.com/reference/releases

How Klorad packages are versioned, released and checked before they reach you.

## Versioning

Every package follows [semantic versioning](https://semver.org) and is versioned on its own:
a release of `@klorad/notify` does not bump `@klorad/react`. While a package is at 0.x, a minor
release may change its public API and a patch release only fixes; from 1.0 a breaking change
needs a major release.

Versions are decided by the commits, not by hand. Every change to the repository is a
[Conventional Commit](https://www.conventionalcommits.org). Before 1.0, `fix:` and `feat:` make
a patch release and a breaking change a minor one; from 1.0, `fix:` makes a patch, `feat:` a
minor and a breaking change a major release.

## Changelogs

Each package ships a `CHANGELOG.md`, written from those commits at release time. Read it in
`node_modules/@klorad/<name>/CHANGELOG.md` before you upgrade.

## What every release passes

- type checking, lint and the package's unit tests, with coverage thresholds of 95% of lines,
  90% of functions and 85% of branches;
- `publint`, which checks the package's `exports`, files and types are consistent;
- Are the Types Wrong, which checks the types resolve for ESM consumers;
- publishing from CI through npm trusted publishing, so no long lived token can publish a
  Klorad package.

The packages are private; see [Install](https://docs.klorad.com/get-started/install) for access.
