diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js
index 4a576b3c2..801cf41c2 100644
--- a/docs/.vitepress/config.js
+++ b/docs/.vitepress/config.js
@@ -106,6 +106,7 @@ export default ({ mode }) => {
{ text: 'Dynamic Properties', link: '/user/dynamic-properties' },
{ text: 'Migration Guide', link: '/user/migration' },
{ text: 'UI Template Examples', link: '/user/template-examples' },
+ { text: 'Multi-Tenancy', link: '/user/multi-tenancy' },
{ text: 'Subflows', link: '/user/subflows' },
{ text: 'Installing on Mobile', link: '/user/pwa' }
]
diff --git a/docs/examples/multi-tenancy.json b/docs/examples/multi-tenancy.json
new file mode 100644
index 000000000..64e384667
--- /dev/null
+++ b/docs/examples/multi-tenancy.json
@@ -0,0 +1,229 @@
+[
+ {
+ "id": "160e82270b278a06",
+ "type": "ui-slider",
+ "z": "a725245cfaf96f83",
+ "group": "32fcb041e36de17e",
+ "name": "",
+ "label": "Single Client",
+ "tooltip": "",
+ "order": 4,
+ "width": 0,
+ "height": 0,
+ "passthru": false,
+ "outs": "all",
+ "topic": "topic",
+ "topicType": "msg",
+ "thumbLabel": true,
+ "min": 0,
+ "max": 10,
+ "step": 1,
+ "className": "",
+ "x": 370,
+ "y": 60,
+ "wires": [
+ [
+ "fa633410a8a67c23"
+ ]
+ ]
+ },
+ {
+ "id": "fa633410a8a67c23",
+ "type": "ui-chart",
+ "z": "a725245cfaf96f83",
+ "group": "32fcb041e36de17e",
+ "name": "",
+ "label": "chart",
+ "order": 6,
+ "chartType": "line",
+ "category": "topic",
+ "categoryType": "msg",
+ "xAxisProperty": "",
+ "xAxisPropertyType": "msg",
+ "xAxisType": "time",
+ "yAxisProperty": "",
+ "ymin": "",
+ "ymax": "",
+ "showLegend": true,
+ "removeOlder": 1,
+ "removeOlderUnit": "3600",
+ "removeOlderPoints": "",
+ "colors": [
+ "#1f77b4",
+ "#aec7e8",
+ "#ff7f0e",
+ "#2ca02c",
+ "#98df8a",
+ "#d62728",
+ "#ff9896",
+ "#9467bd",
+ "#c5b0d5"
+ ],
+ "width": 6,
+ "height": 8,
+ "className": "",
+ "x": 550,
+ "y": 120,
+ "wires": [
+ []
+ ]
+ },
+ {
+ "id": "8a9f5a7b055210a9",
+ "type": "change",
+ "z": "a725245cfaf96f83",
+ "name": "Remove _client",
+ "rules": [
+ {
+ "t": "delete",
+ "p": "_client",
+ "pt": "msg"
+ }
+ ],
+ "action": "",
+ "property": "",
+ "from": "",
+ "to": "",
+ "reg": false,
+ "x": 360,
+ "y": 120,
+ "wires": [
+ [
+ "fa633410a8a67c23"
+ ]
+ ]
+ },
+ {
+ "id": "8934d174f3845df7",
+ "type": "ui-slider",
+ "z": "a725245cfaf96f83",
+ "group": "32fcb041e36de17e",
+ "name": "",
+ "label": "Send to All Users",
+ "tooltip": "",
+ "order": 2,
+ "width": 0,
+ "height": 0,
+ "passthru": false,
+ "outs": "all",
+ "topic": "topic",
+ "topicType": "msg",
+ "thumbLabel": true,
+ "min": 0,
+ "max": 10,
+ "step": 1,
+ "className": "",
+ "x": 130,
+ "y": 120,
+ "wires": [
+ [
+ "8a9f5a7b055210a9"
+ ]
+ ]
+ },
+ {
+ "id": "31b0d54757860d1b",
+ "type": "ui-slider",
+ "z": "a725245cfaf96f83",
+ "group": "32fcb041e36de17e",
+ "name": "Send to Same User",
+ "label": "Send to Same User",
+ "tooltip": "",
+ "order": 3,
+ "width": 0,
+ "height": 0,
+ "passthru": false,
+ "outs": "all",
+ "topic": "topic",
+ "topicType": "msg",
+ "thumbLabel": true,
+ "min": 0,
+ "max": 10,
+ "step": 1,
+ "className": "",
+ "x": 130,
+ "y": 180,
+ "wires": [
+ [
+ "de0801fb73bdceb3"
+ ]
+ ]
+ },
+ {
+ "id": "de0801fb73bdceb3",
+ "type": "change",
+ "z": "a725245cfaf96f83",
+ "name": "Remove socketId",
+ "rules": [
+ {
+ "t": "delete",
+ "p": "_client.socketId",
+ "pt": "msg"
+ }
+ ],
+ "action": "",
+ "property": "",
+ "from": "",
+ "to": "",
+ "reg": false,
+ "x": 350,
+ "y": 180,
+ "wires": [
+ [
+ "fa633410a8a67c23"
+ ]
+ ]
+ },
+ {
+ "id": "32fcb041e36de17e",
+ "type": "ui-group",
+ "name": "Group Name",
+ "page": "e857a4a5a6b5320f",
+ "width": "6",
+ "height": "1",
+ "order": 1,
+ "showTitle": true,
+ "className": "",
+ "visible": true,
+ "disabled": false
+ },
+ {
+ "id": "e857a4a5a6b5320f",
+ "type": "ui-page",
+ "name": "Page Name",
+ "ui": "c851adb9b2a29c9e",
+ "path": "/",
+ "icon": "",
+ "layout": "grid",
+ "theme": "35ee7753b5b3599b",
+ "order": 3,
+ "className": "",
+ "visible": false,
+ "disabled": false
+ },
+ {
+ "id": "c851adb9b2a29c9e",
+ "type": "ui-base",
+ "name": "Multi User Dashboard",
+ "path": "/dashboard",
+ "includeClientData": true,
+ "acceptsClientConfig": [
+ "ui-notification",
+ "ui-control",
+ "ui-template",
+ "ui-chart"
+ ]
+ },
+ {
+ "id": "35ee7753b5b3599b",
+ "type": "ui-theme",
+ "name": "Theme Name",
+ "colors": {
+ "surface": "#16234b",
+ "primary": "#1d44b9",
+ "bgPage": "#ecf2f8",
+ "groupBg": "#ffffff",
+ "groupOutline": "#cccccc"
+ }
+ }
+]
\ No newline at end of file
diff --git a/docs/user/multi-tenancy.md b/docs/user/multi-tenancy.md
new file mode 100644
index 000000000..3d9a9f318
--- /dev/null
+++ b/docs/user/multi-tenancy.md
@@ -0,0 +1,77 @@
+
+
+# Building Multi-Tenant Dashboards
+
+We have two fundamental design patterns when building a Dashboard:
+
+- **Single Source of Truth:** All users of your Dashboard will see the same data. This is useful for industrial IoT or Home Automation applications.
+- **Multi Tenancy:** Data shown in a particular widget is unique to a given client/session/user. This represents a more traditional web application, where each user has their own session and associated data.
+
+By default, any data passed to a Dashboard node is broadcast to all connected clients, and therefore visible to all users ("Single Source of Truth"). In Industrial IoT type use-cases, this is very useful where you may have a chart showing data about a given piece of equipment.
+
+In other cases though, you may wish to send data to just a single client/user, this is where multi-tenancy comes in, and allows you to define constraints on which clients receive particular data.
+
+You can read more about the design patterns [here](../getting-started.md#design-patterns).
+
+## Understanding Client Data
+
+With "Include Client Data" enabled, every `msg` a node emits will have a `_client` object appended to it. This object will detail any available information about the client/user interacting with a given Dashboard.
+
+### Core Client Data
+
+Out of the box, Dashboard will append two piece of information to the `_client` object:
+
+- `socketId`: The unique ID of the socket connection that the client is using to interact with the Dashboard.
+- `socketIp`: The IP address of the client interacting with the Dashboard.
+
+### Plugin Data Providers
+
+Plugins, such as the [FlowFuse User Addon](https://flowfuse.com/blog/2024/04/displaying-logged-in-users-on-dashboard/), are available to append additional information to the `_client` object.
+
+These Authentication plugins can be installed via the Node-RED Palette Manager, and often require Node-RED to be setup with a given Authentication provider, separately from Dashboard. For example, for the FlowFuse User Addon, it is a requirement for Node-RED to be running on FlowFuse with the ["FlowFuse User Authentication"](https://flowfuse.com/docs/user/instance-settings/#flowfuse-user-authentication) option enabled.
+
+The plugins will append additional information to the `_client` object, such as the `user` object, which details the user's name, email address, and any other information that the Authentication provider has available.
+
+## Configuring Client Data
+
+In the [Dashboard sidebar](./sidebar.md#client-data) within the Node-RED Editor, you will find the "Client Data" tab:
+
+
+Screenshot of an example "Client Data" tab
+
+Client data defines information on the user/client interacting with the Dashboard. This data can be appended to every `msg` a node emits, underneath teh `msg._client` object.
+
+When "Include Client Data" is enabled, every `msg._client` will detail the `socketId` and `socketIp` of any connected users.
+
+## Simple Example
+
+Here we demonstrate three different clients opening the same Dashboard.
+
+The Dashboard consists of three sliers and a chart, and is running on FlowFuse, with the "FlowFuse User Addon" plugin enabled.
+
+Two of the clients are logged in as the same user, and the third, "Another User".
+
+
+
+We can see how it's possible to control the interaction of a widget, and how the data emitted from that widget is shared to other components and clients.
+
+The first, "Send to All Users" slider pass through a change node which removes the `_client` object from the message, meaning that the data is sent to all clients, as no constraints are defined.
+
+The second, "Send to Same User" slider passes through a change node, and has the `socketId` field removed from `msg._client`, leaving just the `user` object. This means the data is sent to any connected clients where the same authenticated user is found.
+
+Finally, the "Single Client" slider just passes it's default output to the "Chart" node, including the full `msg._client` object. This means that the data is only sent to the client (`socketId`) & user (`user`) that interacted with the slider.
+
+Below you will find the flow that runs the above example:
+
+
diff --git a/nodes/config/ui_base.js b/nodes/config/ui_base.js
index 82859f529..c97f6b38e 100644
--- a/nodes/config/ui_base.js
+++ b/nodes/config/ui_base.js
@@ -349,8 +349,8 @@ module.exports = function (RED) {
// we only send comms on the connection that matches that id
checks.push(msg._client?.socketId === conn.id)
}
- // if ANY check says this is valid - we send the msg
- return !checks.length || checks.includes(true)
+ // ensure all checks validate sending this
+ return !checks.length || !checks.includes(false)
}
/**