@vustudio/fabric
v0.5.0
Published
TypeScript SDK for Vū Protocol — the real-time communication fabric that connects Vū Intelligence, the Recommendation Engine, and the physical systems they coordinate. Built on MQTT.
Maintainers
Readme
@vustudio/fabric
The TypeScript SDK for Vū Protocol — the real-time communication fabric that connects Vū Intelligence, the Recommendation Engine, and the physical systems they coordinate.
Vū Fabric gives every room, display, light, camera, microphone, sensor, media asset, service, application, and AI agent a shared way to communicate. Each participant can publish state, receive commands, emit events, expose presence, and react to the environment in real time.
At the platform level, Fabric is the nervous system of the Vū Intelligence Stack. The Recommendation Engine senses context and surfaces what the next moment may need. Vū Intelligence routes that context through specialized AI agents. Fabric connects those decisions to the real world — displays, lights, audio, sensors, media players, control systems, and applications.
Built on MQTT, an industry-standard publish/subscribe protocol used across IoT, industrial automation, and complex real-time orchestration environments, Fabric brings proven Industry 4.0 communication patterns into media, entertainment, enterprise experience, and Physical AI.
Where most real-time systems hand developers raw messages and leave the conventions to them, Fabric ships with the conventions built in: a typed unified namespace, automatic delivery semantics, optional schema validation, and one consistent wire format across apps, services, devices, and agents.
MQTT moves the messages. Vū Protocol defines what they mean. Fabric makes the protocol easy to use.
Fabric is designed for Vū deployments and is compatible with MQTT 3.1.1 or 5.0 brokers supported by mqtt.js.
🧭 Why MQTT
Vū deployments are not simple one-application-to-one-device systems.
They are live environments where displays, lighting, audio, cameras, microphones, control surfaces, media players, sensors, cloud services, and AI agents all need to publish state, receive commands, emit events, and react to each other in real time.
MQTT is well suited to this model because it uses a lightweight publish/subscribe architecture, supports many producers and consumers, and is already standardized and widely deployed in connected-device and industrial automation environments.
Vū brings that same pattern into media and entertainment.
Instead of brittle point-to-point integrations, every entity in a Vū deployment participates in a shared namespace:
vu/{site}/{entity}/{id}/{class}/{name}This is the foundation of Vū Protocol: every room, sensor, display, lighting fixture, media asset, service, and AI agent becomes addressable through the same real-time fabric.
✨ Highlights
Industry-standard foundation
Built on MQTT, Fabric gives Vū projects a proven real-time transport layer without locking the platform into a proprietary message bus.
Vū-native unified namespace
Fabric turns MQTT topics into a Vū-specific communication contract. Rooms, walls, devices, sensors, media assets, services, and AI agents all communicate through the same structured namespace.
Delivery semantics by message class
The first segment of every sub-path determines how the message behaves.
| Class | QoS | Retained | Use for |
| ---------------- | --- | -------- | ------------------------- |
| state/... | 1 | yes | Last-known values |
| cmd/... | 1 | no | Imperative actions |
| event/... | 1 | no | Ephemeral occurrences |
| dt/... | 0 | no | High-frequency telemetry |
| telemetry/... | 0 | no | High-frequency telemetry |
| presence/... | 1 | yes | Online / offline state |
| config/... | 1 | yes | Capability / version info |
Application code does not need to remember MQTT options for each message type. The protocol contract applies them automatically.
Typed payload contracts
Optional Zod schemas provide typed .send() calls and typed .on() handlers.
Invalid outgoing payloads are rejected before publish. Invalid incoming payloads are logged and skipped before they reach application code.
Built for real-time orchestration
Fabric handles lazy connection, reconnect behavior, auth failure handling, wildcard subscription pacing, JSON parsing, retained-message clearing, and status events.
One protocol across Vū systems
The same topic contract can be used by browser apps, Node services, device adapters, control systems, and future runtime ports. Every system speaks the same wire format.
🧰 Technology Stack
| Layer | Technologies |
| ------------ | ------------------------------------------------------------------------------------------------------------- |
| Protocol | MQTT 3.1.1 / 5.0 via mqtt.js |
| Vū Protocol | Unified namespace, class contracts, delivery semantics, and typed topic structure for Vū deployments |
| Runtime | Node.js 18+ / Bun / Browser / Deno |
| Language | TypeScript 5 |
| Validation | Zod 3 peer dependency, optional per topic |
| Build | tsup 8 |
| Test | Vitest 2, EMQX service container |
| Tooling | Biome, Changesets, pnpm, tsx |
🏗️ Architecture
@vustudio/fabric is the TypeScript implementation surface for Vū Protocol.
Vū Protocol is the communication fabric that makes every entity in a Vū deployment addressable: rooms, displays, lighting fixtures, sensors, microphones, media assets, AI agents, services, and control surfaces.
Fabric provides the developer API for that protocol. It maps Vū's unified namespace into typed topic handles, applies the correct delivery semantics for each message class, validates payloads when schemas are provided, and manages the MQTT connection lifecycle underneath.
The result is a real-time system where applications do not need to know about each other directly. They only need to publish and subscribe to the shared Vū namespace.
graph TD
subgraph Apps["Vū applications, devices, and agents"]
R["React / Vue / Svelte apps"]
N["Node services"]
W["Cloudflare Workers / edge runtimes (planned)"]
E["ESP32 / device adapters (planned)"]
A["AI agents"]
P["Python sensors (planned)"]
end
subgraph Fabric["@vustudio/fabric"]
L2["Planned Entity Layer<br/>Shadow · Room · Presence · Method"]
L1["Layer 1 — Unified Namespace<br/>vu/{site}/{entity}/{id}/{class}/{name}"]
end
Broker(("MQTT Broker"))
R --> L2
L2 --> L1
N --> L1
W --> L1
E --> L1
A --> L1
P --> L1
L1 <--> BrokerThe SDK is built on two layers.
Layer 1 is the protocol address: the shared wire format used across runtimes.
Layer 2 is an optional higher-level entity model for richer abstractions such as Shadow, Room, Presence, and Method. These entities resolve back to Layer 1 topics underneath.
Use Layer 1 when you need direct protocol control. Use Layer 2 when you want a higher-level developer experience.
📁 Project Structure
src/
├── index.ts Public API surface
├── client.ts MQTT connection lifecycle
├── topic.ts Topic handle: .on / .send / .off / .close
├── classes.ts Message class to QoS / retain contract
├── topic-match.ts MQTT wildcard matcher
├── config.ts Environment-based config loader
└── __tests__/ Unit tests
examples/
├── smoke.ts Publish / subscribe round trip
├── retained.ts Retained delivery example
└── clear-retained.ts Retained cleanup example
docs/
├── getting-started.md
├── concepts.md
├── api.md
├── recipes.md
├── troubleshooting.md
└── faq.md🧠 Core Concepts
Vū Protocol
The communication fabric and unified namespace that defines what messages mean inside a Vū deployment.
Fabric
The TypeScript SDK that implements Vū Protocol for JavaScript and TypeScript applications.
Topic
A scoped pub/sub surface for one entity, such as a room, wall, kiosk, light, camera, service, or agent.
const wall = fabric.topic("playlist/wall-1738");Sub-path
The path beneath a topic.
state/queue
cmd/play
event/clipFinished
dt/fps
presence/statusThe first segment is the message class.
Class
The message class determines delivery behavior.
For example, state/... is retained, event/... is ephemeral, and telemetry/... is optimized for high-frequency updates.
Schema
An optional Zod schema keyed by sub-path.
Schemas validate outgoing payloads before publish and incoming payloads before handler execution.
🚀 Quick Start
1. Install
pnpm add @vustudio/fabric mqtt zodmqtt and zod are peer dependencies.
2. Configure
# .env
VU_MQTT_BROKER=wss://your-broker.example.com/mqtt
VU_MQTT_USERNAME=
VU_MQTT_PASSWORD=Or pass config explicitly:
import { createFabric } from "@vustudio/fabric";
const fabric = createFabric({
broker: process.env.VU_MQTT_BROKER!,
username: process.env.VU_MQTT_USERNAME,
password: process.env.VU_MQTT_PASSWORD,
});3. Speak Vū Protocol
import { fabric } from "@vustudio/fabric";
const wall = fabric.topic("playlist/wall-1738");
await wall.on("state/queue", (queue) => {
console.log("queue:", queue);
});
await wall.send("cmd/play", { clipId: 42 });
await wall.send("state/queue", [
{ id: 1, title: "Hero" },
]);The SDK connects on first use, parses JSON payloads, and applies the correct delivery semantics for each message class.
4. Add types with Zod
import { z } from "zod";
import { fabric } from "@vustudio/fabric";
const wall = fabric.topic("playlist/wall-1738", {
schema: {
"state/queue": z.array(
z.object({
id: z.number(),
title: z.string(),
}),
),
"cmd/play": z.object({
clipId: z.number(),
}),
},
});
await wall.send("cmd/play", { clipId: 42 });
// Type error
await wall.send("cmd/play", { wrong: "thing" });🎯 Scope a namespace once
Real deployments address things through a long, structured prefix — tenant, use case, entity, id. Repeating that prefix by hand invites drift: the same path gets re-typed slightly differently in three places, and now messages miss each other.
scope() fixes the prefix once and derives every topic, send, and subscribe from it. For the Experience namespace specifically, fabric.experience() builds that prefix for you:
import { fabric } from "@vustudio/fabric";
// Open your experience's scope. tenant defaults to "default" — a reserved
// single-tenant slot (mirrors Kubernetes' own `default` namespace). Pass a
// real tenant only once this experience needs multi-tenant isolation; it's
// a rename + a new ACL grant then, not a re-architecture.
const exp = fabric.experience("responsible-ai-simulation");
// → vu/default/experience/responsible-ai-simulation
// Nest for a specific entity — joins and trims slashes for you.
const session = exp.scope("session", sessionId);
// → vu/default/experience/responsible-ai-simulation/session/4f2a
// send / on / clear are relative to the scope. Sub-paths are class/name only.
await session.send("state/snapshot", snapshot); // QoS 1 + retained, from the class
await session.on("event/rubric-hit", (hit, fullTopic) => { /* ... */ });
await session.clear("state/snapshot"); // remove the retained slotAttach Zod schemas at topic() — one scope spans many class/name pairs:
const session = exp.scope("session", sessionId).topic({
schema: {
"state/snapshot": snapshotSchema,
"event/rubric-hit": rubricHitSchema,
},
});
await session.send("state/snapshot", snapshot); // typed and validatedFor anything outside the Experience namespace, fabric.scope(base) still takes any prefix directly.
The scope prefix is also the isolation boundary: each use case is ACL-scoped to its own vu/{tenant}/experience/{usecase}/# subtree on the broker. One prefix, one home — see the recipe.
Connection safety
send() waits for the broker and rejects if it cannot connect within connectTimeoutMs (default 10s) — it never hangs. When you would rather not handle a rejection, use trySend(), which resolves false if the client is not connected:
if (!fabric.isConnected()) {
// show an offline indicator, queue locally, etc.
}
const sent = await session.trySend("cmd/play", { clipId: 42 });
if (!sent) {
// not connected — decide what to do (retry, drop, buffer)
}trySend() never throws — if the payload fails class or schema validation, the error is routed to your onError hook and trySend() resolves false.
Sharing one connection with client injection
Some consumers already need a raw mqtt.js client of their own — e.g. one physical connection carrying both Fabric's class-contract JSON topics and a consumer's own binary (non-JSON) topics, with a single Last-Will. Pass that client to createFabric({ client }) instead of letting Fabric open its own socket:
import mqtt from "mqtt";
import { createFabric } from "@vustudio/fabric";
const mqttClient = mqtt.connect("wss://mqtt.vu.studio/mqtt", {
username: "...",
password: "...",
will: { topic: "presence/voice-bridge", payload: "offline", qos: 1, retain: true },
});
// Fabric attaches its schema/class-contract layer on top of your socket. It
// never calls connect()/end() on it — you created it, you own its lifecycle.
const fab = createFabric({ client: mqttClient });
const room = fab.topic("room/1");
await room.send("state/snapshot", snapshot); // Fabric's class contract, same socket
// Your own binary topics ride the same connection, fully outside Fabric:
mqttClient.publish("room/1/audio/opus", opusFrame, { qos: 0 });Rules:
brokeris optional whenclientis provided.username,password, andwillare meaningless once a client already exists — passing any of them together withclientthrows at construction. Set them on your ownmqtt.connect()call instead.fab.close()on an injected client only detaches Fabric's listeners — it never calls.end(). You created the socket, you close it.- Reconnect and resubscription behavior follow your
mqtt.jsoptions (mqtt.jsdefaultsresubscribe: true, so Fabric'son()subscriptions survive a reconnect by default — butresubscribe: falseorclean: falseare yours to own). fab.getMqttClient()(andFabricClient.getMqttClient()) returns the underlying client — the one you injected, or the SDK-created one once a connect/publish/subscribe has triggered it.
For an SDK-created connection (no client injected), set a Last-Will via createFabric({ broker, will }) — it's passed straight through to mqtt.connect()'s IClientOptions.
🧪 Local Development
1. Install
git clone [email protected]:vustudio/vu-fabric.git
cd vu-fabric
pnpm install
cp .env.example .env2. Run checks
pnpm checkThis runs linting, typecheck, tests, and build.
| Task | Command |
| :---------- | :--------------- |
| Lint | pnpm lint |
| Typecheck | pnpm typecheck |
| Test | pnpm test |
| Build | pnpm build |
| Watch build | pnpm dev |
| Smoke test | pnpm smoke |
3. Run a local broker
docker compose up -dPoint your local environment at the broker:
VU_MQTT_BROKER=ws://localhost:8083/mqtt
VU_MQTT_USERNAME=
VU_MQTT_PASSWORD=Run the smoke test:
pnpm smokeLocal EMQX dashboard: http://localhost:18083
📦 Publishing
Releases are managed with Changesets.
1. Add a changeset
pnpm changesetChoose patch, minor, or major, write a short summary, and commit the generated file with your PR.
2. Release
The release workflow consumes pending changesets, updates package versions, writes the changelog, builds the package, and publishes to npm.
Authors should not manually edit package versions unless repairing the release process.
📚 Documentation
🗺️ Roadmap
The current package delivers the core topic contract, namespace scoping (scope()), and connection-safe publishing (trySend() / isConnected()).
Planned areas include:
- presence helpers for Last Will and Testament, birth, and death messages
- React bindings for topic subscriptions and cleanup
- CLI tools for watching, snapshotting, and auditing topics
- wildcard publish protection
- Cloudflare Worker support
- framework adapters for Vue and Svelte
- ESP32 and Python packages that follow the same topic shape
- higher-level entities such as Shadow, Room, and Method
- developer tools for inspecting traffic, retained snapshots, and presence state
The roadmap may change as Vū Protocol and the deployment model mature.
🚦 Status
This package is pre-1.0.
The topic contract is intended to be stable, but API changes may still occur before the first major release. For production deployments, pin versions and review release notes before upgrading.
Fabric is the SDK. Vū Protocol is the language. MQTT is the transport.
