diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/EquipmentNodeIds.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/EquipmentNodeIds.cs
new file mode 100644
index 00000000..f6e1c724
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/EquipmentNodeIds.cs
@@ -0,0 +1,31 @@
+namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
+
+///
+/// Single source of truth for equipment-namespace OPC UA NodeId strings. The variable NodeId is
+/// FOLDER-SCOPED ({parent}/{Name}), NOT the driver-side FullName — a driver wire ref is not
+/// unique across identical machines, so FullName-as-NodeId would collide in the sink. Used by the
+/// materialiser (Phase7Applier), the VirtualTag publish map, and the driver live-value router so all
+/// three agree on the exact NodeId a variable was placed at.
+///
+public static class EquipmentNodeIds
+{
+ /// The sub-folder NodeId under an equipment for a non-empty FolderPath: {equipmentId}/{folderPath}.
+ /// The owning equipment's NodeId.
+ /// The tag/vtag FolderPath (must be non-empty for this to be meaningful).
+ /// The sub-folder NodeId string.
+ public static string SubFolder(string equipmentId, string folderPath) => $"{equipmentId}/{folderPath}";
+
+ ///
+ /// The folder-scoped variable NodeId: {parent}/{name} where parent = equipmentId when
+ /// is null/empty, else .
+ ///
+ /// The owning equipment's NodeId.
+ /// The tag/vtag FolderPath, or null/empty for "directly under the equipment".
+ /// The tag/vtag Name (the leaf browse segment).
+ /// The folder-scoped variable NodeId string.
+ public static string Variable(string equipmentId, string? folderPath, string name)
+ {
+ var parent = string.IsNullOrWhiteSpace(folderPath) ? equipmentId : SubFolder(equipmentId, folderPath);
+ return $"{parent}/{name}";
+ }
+}
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/EquipmentNodeIdsTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/EquipmentNodeIdsTests.cs
new file mode 100644
index 00000000..4601771c
--- /dev/null
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/EquipmentNodeIdsTests.cs
@@ -0,0 +1,24 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
+
+namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.OpcUa;
+
+public class EquipmentNodeIdsTests
+{
+ [Fact]
+ public void Variable_with_no_folder_is_equipment_slash_name()
+ => EquipmentNodeIds.Variable("eq-1", "", "speed").ShouldBe("eq-1/speed");
+
+ [Fact]
+ public void Variable_with_null_folder_is_equipment_slash_name()
+ => EquipmentNodeIds.Variable("eq-1", null, "speed").ShouldBe("eq-1/speed");
+
+ [Fact]
+ public void Variable_with_folder_is_equipment_slash_folder_slash_name()
+ => EquipmentNodeIds.Variable("eq-1", "registers", "speed").ShouldBe("eq-1/registers/speed");
+
+ [Fact]
+ public void SubFolder_is_equipment_slash_folder()
+ => EquipmentNodeIds.SubFolder("eq-1", "registers").ShouldBe("eq-1/registers");
+}