Web / Node.js Matter SDK

A guide to Matter development with matter.js, covering Node.js controllers, browser proxies, and device bridging.

Matter 1.4.2 — 1.6Node.js 20.19+Production

Overview

matter.js is a pure TypeScript implementation of the Matter protocol, maintained by the Open Home Foundation. It can build controllers, virtual devices, and bridges. Home Assistant adopted it as their production Matter controller in 2026.7. Note: matter.js has not yet received official CSA certification; certification efforts are ongoing.

Key point: browsers cannot communicate directly with Matter devices because Matter uses UDP and browsers have no raw UDP socket access. The practical solution is to run matter.js on a Node.js server and proxy browser requests via WebSocket.

Architecture & Packages

Core packages and usage patterns in the matter.js ecosystem:

PackagePurposeRuntime
@matter/mainPrimary entry point, Behavior APINode.js / Bun
@matter/nodejsNode.js platform adapter (UDP / mDNS / BLE)Node.js
@matter/nodeServerNode / Endpoint / BehaviorNode.js
@matter-server/ws-clientBrowser-side WebSocket clientBrowser / Node.js
@matter-server/ws-controllerServer-side Matter controllerNode.js
matterbridgePlugin-based Matter bridge frameworkNode.js

Browser Limitations

The browser security model fundamentally prevents direct Matter protocol execution:

No Raw UDP Sockets

Matter runs on UDP port 5540. Browsers have no raw UDP socket API. This is a security model constraint, not a missing feature.

No mDNS/DNS-SD

Matter uses multicast DNS for device discovery. Browsers cannot send or receive multicast UDP packets.

Insufficient BLE Access

Web Bluetooth requires a user-gesture device chooser dialog and cannot perform the silent BLE scanning and PASE sessions that Matter requires.

No Thread Configuration

Browsers have no Thread network API and cannot configure Thread device credentials.

WebSocket Proxy Pattern

matterjs-server provides a production-proven browser access pattern. The Node.js server holds the full Matter controller; browsers control devices remotely via WebSocket.

typescriptBrowser-Side Device Control
import { MatterClient } from "@matter-server/ws-client";

// Connect to local matterjs-server
const client = new MatterClient("ws://localhost:5580/ws");
await client.startListening();

// View commissioned devices
const nodes = client.nodes;

// Send device command: toggle light (endpoint 1, cluster 6 = OnOff)
await client.deviceCommand(nodeId, 1, 6, "toggle");

// Listen for device state changes
client.addEventListener("nodes_changed", (event) => {
    console.log("State changed:", event);
});

Node.js Controller

In a Node.js environment, matter.js can run a full Matter controller directly with no proxy layer needed:

typescriptNode.js Direct Control
import { CommissioningController } from "@matter/main";
import { OnOff } from "@matter/main/clusters";

// Create and start controller
const controller = new CommissioningController({
    environment: { environment, id: "MyController" },
    autoConnect: true,
});
await controller.start();

// Get commissioned device endpoint
const node = controller.getNode(nodeId);
const endpoint = node.getEndpoint(1);

// Control device via Cluster API
const onOffCluster = endpoint.getClusterClient(OnOff.Cluster);
await onOffCluster.toggle();

Reading device types and capabilities

matterjs-server has already read every attribute after commissioning, so the browser just picks values from the node data by endpoint/cluster/attribute. Paste that data into the JSON Parser to see a ready-made device profile; see Concepts · Reading a device's capabilities for what each field means.

typescriptReading device types and capabilities from node data
import { MatterClient } from "@matter-server/ws-client";

const client = new MatterClient("ws://localhost:5580/ws");
await client.startListening();

// After commissioning the server does a wildcard read; the node data holds every raw attribute
// Keys are "endpoint/cluster/attribute" (decimal), the same format as Home Assistant diagnostics
const attrs = client.nodes[nodeId].attributes;

const endpoints = attrs["0/29/3"];          // Descriptor.PartsList → [1]
for (const ep of endpoints) {
  const types = attrs[`${ep}/29/0`];        // DeviceTypeList → [{ "0": 10, "1": 3 }]  10 = 0x000A Door Lock
  const servers = attrs[`${ep}/29/1`];      // ServerList → [3, 29, 47, 257]
  console.log(ep, types, servers);
}

const vendorName = attrs["0/40/1"];         // BasicInformation.VendorName
const lockFeatures = attrs["1/257/65532"];  // DoorLock.FeatureMap

Resources