KloradDocs

Give a team its own view

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.

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.

Run it liveSandbox: WorldsOne twin, entitled views: the city administrator sees every bin, a district crew sees and acts on its own.

Something wrong or unclear? Tell your contact at Prieston Technologies.