Matterbridge Logo   Matterbridge development

npm version npm downloads Docker Version Docker Pulls Node.js CI CodeQL Codecov tested with Vitest styled with Oxc linted with Oxc TypeScript TypeScript Native ESM matterbridge.io

powered by powered by powered by


Development

How to create your plugin

The easiest way is to clone the Matterbridge Plugin Template that has Dev Container support for instant development environment and all tools and extensions (like Node.js, npm, TypeScript, Jest, Vitest, and the shared Matterbridge Oxc/oxlint/oxfmt configs) already loaded and configured.

After you clone it locally, change the name (keep always matterbridge- at the beginning of the name), version, description, author, homepage, repository, bugs and funding in the package.json.

It is also possible to add two custom properties to the package.json: help and changelog with a url that will be used in the frontend instead of the default (/blob/main/README and /blob/main/CHANGELOG).

Add your plugin logic in module.ts.

The Matterbridge Plugin Template has already configured Jest and Vitest test suites (with 100% coverage) that you can expand while you add your own plugin logic.

It also has a workflow configured to run on push and pull request that build, lint and test the plugin on node 20, 22 and 24 with ubuntu, macOS and windows.

Matterbridge Dev Container

Using a Dev Container provides a fully isolated, reproducible, and pre-configured development environment. This ensures that all contributors have the same tools, extensions, and dependencies, eliminating "works on my machine" issues. It also makes onboarding new developers fast and hassle-free, as everything needed is set up automatically.

For improved efficiency, the setup uses named Docker volumes for .cache and node_modules. This means dependencies are installed only once and persist across container rebuilds, making installs and rebuilds much faster than with bind mounts or ephemeral volumes.

To start the Dev Container, simply open the project folder in Visual Studio Code and, if prompted, click "Reopen in Container". Alternatively, use the Command Palette (Ctrl+Shift+P or Cmd+Shift+P), search for "Dev Containers: Reopen in Container", and select it. VS Code will automatically build and start the containerized environment for you.

Note: The first time you use the Dev Container, it may take a while to download all the required Docker images and set up the environment. Subsequent starts will be as fast as opening the local folder.

Matterbridge Plugin Dev Container

Using a Dev Container provides a fully isolated, reproducible, and pre-configured development environment. This ensures that all contributors have the same tools, extensions, and dependencies, eliminating "works on my machine" issues. It also makes onboarding new developers fast and hassle-free, as everything needed is set up automatically.

For improved efficiency, the setup uses named Docker volumes for matterbridge, .cache and node_modules. This means that the dev of matterbridge and the plugin dependencies are installed only once and persist across container rebuilds, making installs and rebuilds much faster than with bind mounts or ephemeral volumes. The plugin is automatically added to Matterbridge instance installed inside the dev container.

To start the Dev Container, simply open the project folder in Visual Studio Code and, if prompted, click "Reopen in Container". Alternatively, use the Command Palette (Ctrl+Shift+P or Cmd+Shift+P), search for "Dev Containers: Reopen in Container", and select it. VS Code will automatically build and start the containerized environment for you.

Note: The first time you use the Dev Container, it may take a while to download all the required Docker images and set up the environment. Subsequent starts will be as fast as opening the local folder.

How to pair matterbridge in Dev Containers

Dev containers have networking limitations depending on the host OS and Docker setup.

• Docker Desktop on Windows or macOS:

• Native Linux or WSL 2 with Docker Engine CLI integration:

Copilot instructions

File Notes
.github/copilot-instructions.html Main project instructions — always loaded
.github/instructions/matterbridge/matterbridge.instructions.html Matterbridge endpoint guide — dedicated Copilot instruction file
.github/instructions/testing/unit-tests.instructions.html Testing standards — scoped to **/*.test.ts

Claude instructions

File Notes
CLAUDE.html Main project instructions — always loaded
.claude/rules/matterbridge/matterbridge.instructions.html Matterbridge endpoint guide — loaded for all contexts
.claude/rules/testing/unit-tests.instructions.html Testing standards — scoped to **/*.test.ts

Agents instructions

File Notes
AGENTS.html Main project instructions
.codex/config.toml Codex project permissions, approvals, and profile
.codex/rules/default.rules Codex command allow, prompt, and deny rules

Guidelines on imports/exports

Matterbridge exports from:

"matterbridge"

"matterbridge/devices"

"matterbridge/clusters"

"matterbridge/behaviors"

"matterbridge/utils"

"matterbridge/logger"

"matterbridge/storage"

"matterbridge/dgram"

"matterbridge/jestutils"

"matterbridge/jest-utils"

"matterbridge/jest-utils/matter"

"matterbridge/vitest-utils"

"matterbridge/vitest-utils/matter"

"matterbridge/matter"

"matterbridge/matter/behaviors"

"matterbridge/matter/clusters"

"matterbridge/matter/devices"

"matterbridge/matter/endpoints"

"matterbridge/matter/model"

"matterbridge/matter/types"

****** WARNING ******

A plugin must never install or import from @matter or @project-chip directly (neither as a dependency, devDependency, nor peerDependency), as this leads to a second instance of matter.js, causing instability and unpredictable errors such as "The only instance is Endpoint".

Additionally, when Matterbridge updates the matter.js version, it should be consistent across all plugins.

****** WARNING ******

A plugin must never declare Matterbridge as a dependency, devDependency, or peerDependency.

For local development only, Matterbridge may be linked to the plugin with npm link matterbridge. At runtime the plugin is loaded directly from the running Matterbridge instance.

{
  "scripts": {
    "dev:link": "npm link matterbridge"
  }
}

If you don't use Dev Container from the Matterbridge Plugin Template, on the host you use for the development of your plugin, you need to clone matterbridge, build it locally and link it globally (npm link from the matterbridge package root).

git clone --depth 1 --single-branch --no-tags https://github.com/Luligu/matterbridge.git
cd matterbridge
npm install --no-fund --no-audit
npm run build
cd apps/frontend
npm install --no-fund --no-audit
npm run build
cd ../..
npm link --no-fund --no-audit

If you want to develop a plugin using the dev branch of matterbridge (I suggest you do it).

git clone --depth 1 --single-branch --no-tags -b dev https://github.com/Luligu/matterbridge.git
cd matterbridge
npm install --no-fund --no-audit
npm run build
cd apps/frontend
npm install --no-fund --no-audit
npm run build
cd ../..
npm link --no-fund --no-audit

Always keep your local instance of matterbridge up to date.

****** WARNING ******

Some error messages are logged on start when a plugin has invalid imports or configuration and the plugin will be disabled to prevent instability and crashes.

How to install and register a plugin for development (from github)

To install i.e. https://github.com/Luligu/matterbridge-example-accessory-platform

On windows:

cd $HOME\Matterbridge

On linux or macOS:

cd ~/Matterbridge

then clone the plugin

git clone --depth 1 --single-branch --no-tags https://github.com/Luligu/matterbridge-example-accessory-platform
cd matterbridge-example-accessory-platform
npm install --no-fund --no-audit
npm link matterbridge
npm run build

then add the plugin to Matterbridge

matterbridge -add .

MatterbridgeDynamicPlatform and MatterbridgeAccessoryPlatform api

public name: string

The plugin name.

public type: string

The plugin platform type.

public config: object

The plugin config (loaded before the platform constructor is called and saved after onShutdown() is called). Here you can store your plugin configuration (see matterbridge-zigbee2mqtt for example)

constructor(matterbridge: PlatformMatterbridge, log: AnsiLogger, config: PlatformConfig)

The contructor is called when is plugin is loaded.

async onStart(reason?: string)

The method onStart() is where you have to create your MatterbridgeEndpoint and add all needed clusters.

After add the command handlers and subscribe to the attributes when needed.

The MatterbridgeEndpoint class has the create cluster methods already done and all command handlers needed (see plugin examples).

The method is called when Matterbridge load the plugin.

async onConfigure()

The method onConfigure() is where you can configure your matter device.

The method is called when the server node the platform belongs to is online.

Since the persistent attributes are loaded from the storage when the server node goes online, you may need to set them in onConfigure().

async onShutdown(reason?: string)

The method onShutdown() is where you have to stop your platform and cleanup all the used resources.

The method is called when Matterbridge is shutting down or when the plugin is disabled.

Since the frontend can enable and disable the plugin many times, you need to clean all resources (i.e. handlers, intervals, timers...) here.

async onChangeLoggerLevel(logLevel: LogLevel)

It is called when the user changes the logger level in the frontend.

async onAction(action: string, value?: string, id?: string, formData?: PlatformConfig)

It is called when a plugin config includes an action button or an action button with text field.

async onConfigChanged(config: PlatformConfig)

It is called when the plugin config has been updated.

getDevices(): MatterbridgeEndpoint[]

Retrieves the devices registered with the platform.

getDeviceByName(deviceName: string): MatterbridgeEndpoint | undefined

getDeviceByUniqueId(uniqueId: string): MatterbridgeEndpoint | undefined

getDeviceBySerialNumber(serialNumber: string): MatterbridgeEndpoint | undefined

getDeviceById(id: string): MatterbridgeEndpoint | undefined

getDeviceByOriginalId(originalId: string): MatterbridgeEndpoint | undefined

getDeviceByNumber(number: EndpointNumber | number): MatterbridgeEndpoint | undefined

They all return MatterbridgeEndpoint or undefined if not found.

hasDeviceName(deviceName: string): boolean

hasDeviceUniqueId(deviceUniqueId: string): boolean

Checks if a device with this name or uniqueId is already registered in the platform.

async registerDevice(device: MatterbridgeEndpoint)

After you have created your device, add it to the platform.

async unregisterDevice(device: MatterbridgeEndpoint)

You can unregister a device.

async unregisterAllDevices()

You can unregister all the devices you added.

It can be useful to call this method from onShutdown() if you don't want to keep all the devices during development.

MatterbridgeEndpoint api

You create a Matter device with a new instance of MatterbridgeEndpoint(definition: DeviceTypeDefinition | AtLeastOne, options: MatterbridgeEndpointOptions = {}, debug: boolean = false).

const device = new MatterbridgeEndpoint([contactSensor, powerSource], { id: 'EntryDoor' })
  .createDefaultIdentifyClusterServer()
  .createDefaultBasicInformationClusterServer('My entry door', '0123456789')
  .createDefaultBooleanStateClusterServer(true)
  .createDefaultPowerSourceReplaceableBatteryClusterServer(75)
  .addRequiredClusters(); // Always better to call it at the end of the chain to add all the not already created but required clusters (server and client).

In the above example we create a contact sensor device type with also a power source device type feature replaceble battery.

All device types are defined in src\matterbridgeDeviceTypes.ts and taken from the 'Matter-1.4.2-Device-Library-Specification.pdf'.

All default cluster helpers are available as methods of MatterbridgeEndpoint.

MatterbridgeEndpointOptions

The mode=server property of MatterbridgeEndpointOptions, allows to create an independent (not bridged) Matter device with its server node. In this case the bridge mode is not relevant.

The mode=matter property of MatterbridgeEndpointOptions, allows to create a (not bridged) Matter device that is added to the Matterbridge server node alongside the aggregator.

How to use cluster clients

Some Matter device types act as controllers rather than servers. They consume clusters that are implemented on remote endpoints (e.g. a Closure Controller that drives a Closure device). These are called client clusters.

Other devices like OnOffLight have an optional cluster client OccupancySensor that, when bound to another endpoint (local or remote), allows to turn the light on and off following the OccupancySensor status.

Matterbridge exposes client cluster support through MatterbridgeBindingServer. When required on an endpoint it:

  1. Registers the given cluster IDs in the Binding cluster's clientList state.
  2. Syncs those IDs into the Descriptor cluster's clientList attribute so the fabric sees them.
  3. Reacts to established bindings and — for client-kind bindings — enables auto-subscription on the remote node.

Adding client clusters explicitly

Use createDefaultBindingClusterServer(clientList) when you know exactly which cluster IDs to advertise:

import { ClosureControl } from '@matter/types/clusters/closure-control';
import { closureController } from 'matterbridge';

const device = new MatterbridgeEndpoint(closureController, { id: 'MyClosureController' })
  .createDefaultBindingClusterServer([ClosureControl.id]) // advertises ClosureControl as a client cluster
  .addRequiredClusters();

await this.registerDevice(device);

createDefaultBindingClusterServer delegates to addClusterClients and is safe to call multiple times — each call merges the new IDs into the existing list without duplicates. Use addClusterClients(clientList) directly when you need to add client clusters after the initial chain.

Adding client clusters from the device type definition

Each DeviceTypeDefinition carries requiredClientClusters and optionalClientClusters lists, taken directly from the Matter specification. Use these helpers to populate the binding automatically from the device type:

import { closureController } from 'matterbridge';

const device = new MatterbridgeEndpoint(closureController, { id: 'MyClosureController' })
  .addRequiredClusterServers()
  .addRequiredClusterClients() // adds ClosureControl.id (required by closureController)
  .addOptionalClusterClients(); // adds Identify, Groups, ClosureDimension (optional)

await this.registerDevice(device);

MatterbridgeEndpoint single class devices

For the device types listed below there are single class provided to createa a fully functional device.

For a working example refer to the 'matterbridge-example-dynamic-platform'.

Chapter 12. Robotic Device Types - Single class device types

const robot = new RoboticVacuumCleaner('Robot Vacuum', 'RVC1238777820', 'server');

Chapter 13. Appliances Device Types - Single class device types

const laundryWasher = new LaundryWasher('Laundry Washer', 'LW1234567890');
const laundryDryer = new LaundryDryer('Laundry Dryer', 'LDW1235227890');
const dishwasher = new Dishwasher('Dishwasher', 'DW1234567890');
const extractorHood = new ExtractorHood('Extractor Hood', 'EH1234567893');
const microwaveOven = new MicrowaveOven('Microwave Oven', 'MO1234567893');

The Oven is always a composed device. You create the Oven and add one or more cabinet.

const oven = new Oven('Oven', 'OV1234567890');
oven.addCabinet('Upper Cabinet', [{ mfgCode: null, namespaceId: PositionTag.Top.namespaceId, tag: PositionTag.Top.tag, label: PositionTag.Top.label }]);

The Cooktop is always a composed device. You create the Cooktop and add one or more surface.

const cooktop = new Cooktop('Cooktop', 'CT1234567890');
cooktop.addSurface('Surface Top Left', [
  { mfgCode: null, namespaceId: PositionTag.Top.namespaceId, tag: PositionTag.Top.tag, label: PositionTag.Top.label },
  { mfgCode: null, namespaceId: PositionTag.Left.namespaceId, tag: PositionTag.Left.tag, label: PositionTag.Left.label },
]);

The Refrigerator is always a composed device. You create the Refrigerator and add one or more cabinet.

const refrigerator = new Refrigerator('Refrigerator', 'RE1234567890');
refrigerator.addCabinet('Refrigerator Top', [
  { mfgCode: null, namespaceId: PositionTag.Top.namespaceId, tag: PositionTag.Top.tag, label: 'Refrigerator Top' },
  { mfgCode: null, namespaceId: RefrigeratorTag.Refrigerator.namespaceId, tag: RefrigeratorTag.Refrigerator.tag, label: RefrigeratorTag.Refrigerator.label },
]);

Chapter 14. Energy Device Types - Single class device types

const waterHeater = new WaterHeater('Water Heater', 'WH3456177820');
const evse = new Evse('Evse', 'EV3456127820');
const solarPower = new SolarPower('Solar Power', 'SP3456127821');
const batteryStorage = new BatteryStorage('Battery Storage', 'BS3456127822');
const heatPump = new HeatPump('Heat Pump', 'HP1234567890');

Plugin config file

Each plugin has a minimal default config file injected by Matterbridge when it is loaded and the plugin doesn't have its own default one:

{
  name: plugin.name, // i.e. matterbridge-test
  type: plugin.type, // i.e. AccessoryPlatform or DynamicPlatform (on the first run is AnyPlatform cause it is still unknown)
  version: plugin.version,
  debug: false,
  unregisterOnShutdown: false
}

It is possible to add a different default config file to be loaded the first time the user installs the plugin.

Matterbridge (only on the first load of the plugin and if a config file is not already present in the .matterbridge directory) looks for the default config file in the root of the plugin package. The file must be named '[PLUGIN-NAME].config.json' (i.e. 'matterbridge-test.config.json').

In all subsequent loads the config file is loaded from the '.matterbridge' directory.

Plugin schema file

Each plugin has a minimal default schema file injected by Matterbridge when it is loaded and the plugin doesn't have its own default one:

{
  title: plugin.description,
  description: plugin.name + ' v. ' + plugin.version + ' by ' + plugin.author,
  type: 'object',
  properties: {
    name: {
      'title': 'Plugin Name',
      'description': 'Plugin name',
      'type': 'string',
      'readOnly': true,
      'ui:widget': 'hidden',
    },
    type: {
      'title': 'Plugin Type',
      'description': 'Plugin type',
      'type': 'string',
      'readOnly': true,
      'ui:widget': 'hidden',
    },
    version: {
      'title': 'Plugin Version',
      'description': 'Plugin version',
      'type': 'string',
      'readOnly': true,
      'ui:widget': 'hidden',
    },
    debug: {
      title: 'Enable Debug',
      description: 'Enable the debug for the plugin (development only)',
      type: 'boolean',
      default: false,
    },
    unregisterOnShutdown: {
      title: 'Unregister On Shutdown',
      description: 'Unregister all devices on shutdown (development only)',
      type: 'boolean',
      default: false,
    },
  },
}

It is possible to add a different default schema file.

The schema file is loaded from the root of the plugin package. The file must be named '[PLUGIN-NAME].schema.json' (i.e. 'matterbridge-test.schema.json').

The properties of the schema file shall correspond to the properties of the config file.

Deprecation list

Scheduled removal: all symbols listed below will be removed in matterbridge 3.10.0.

Behavior / Server classes

Deprecated symbol Replacement
MatterbridgeEnhancedColorControlServer MatterbridgeColorControlServer with the EnhancedHue feature
MatterbridgePresetThermostatServer MatterbridgeThermostatServer with the Presets feature
MatterbridgeLiftWindowCoveringServer MatterbridgeWindowCoveringServer with Lift + PositionAwareLift features
MatterbridgeLiftTiltWindowCoveringServer MatterbridgeWindowCoveringServer with Lift, PositionAwareLift, Tilt + PositionAwareTilt features

Device type aliases

Deprecated symbol Replacement
onOffOutlet onOffPlugInUnit
dimmableOutlet dimmablePlugInUnit
onOffMountedSwitch mountedOnOffControl
dimmableMountedSwitch mountedDimmableLoadControl
pumpDevice pump
onOffSwitch onOffLightSwitch
dimmableSwitch dimmerSwitch
colorTemperatureSwitch colorDimmerSwitch
doorLockDevice doorLock
coverDevice windowCovering
thermostatDevice thermostat
fanDevice fan
speakerDevice speaker
airConditioner roomAirConditioner

Common namespace tag aliases

Deprecated symbol Replacement
AreaNamespaceTag CommonAreaNamespaceTag
ClosureTag CommonClosureTag
CompassDirectionTag CommonCompassDirectionTag
CompassLocationTag CommonCompassLocationTag
DirectionTag CommonDirectionTag
LandmarkNamespaceTag CommonLandmarkNamespaceTag
LevelTag CommonLevelTag
LocationTag CommonLocationTag
NumberTag CommonNumberTag
PositionTag CommonPositionTag
RelativePositionTag CommonRelativePositionTag

Methods and interfaces

Deprecated symbol Replacement
MatterbridgeEndpoint.getChildEndpointByName() getChildEndpointById() or getChildEndpointByOriginalId()
MatterbridgeEndpointCommands interface CommandHandlers

Package exports

Deprecated export Replacement
matterbridge/jestutils matterbridge/jest-utils

Frequently asked questions

Why plugins cannot install matterbridge as a dependency, devDependency or peerDependency

There must be one and only one instance of Matterbridge and matter.js in the node_modules directory.

What happens when matterbridge or matter.js are present like a devDependencies

The plugins can be globally installed in different ways:

In all these cases the devDependencies are always installed by npm and show up in the plugins node_modules:

In the first 2 cases the devDependeincies are always installed in node_modules!

In the last (most dangerous case) they are installed when the user forgets to add --omit=dev or doesn't have NODE_ENV=production.

This is also the reason why to be safe 100% all official plugins are published for production removing also devDependencies from package.json.

I also lock the direct dependencies with npm shrinkwrap cause npm installs always the latest versions that mach your range in package.json but sometimes this just breaks the plugin. This permits to be sure that the user host machine has exactly the same direct dependencies you coded your plugin with.

The technical reason we cannot have matterbridge or @matter in the plugin node_modules.

Module Resolution in Matterbridge Plugin System.

When Matterbridge loads plugins on demand as ESM modules, the module resolution follows Node.js's standard module resolution algorithm. Here's how it works:

1. Plugin Loading Process From the code in pluginManager.ts (lines 628-632), Matterbridge:

2. Module Resolution Priority When the plugin code runs import statements, Node.js follows this resolution order:

3. Key Behavior Plugin's node_modules takes precedence. If a package exists in the plugin's own node_modules, that version will be used. Matterbridge's node_modules is used as fallback.

Code Style Guidelines and Copilot hints

Read the guideline

Contribution Guidelines

Read the guideline