Skip to main content

Creating an OPC UA server

You can create a working OPC UA server directly from Node-RED using only Function nodes — no settings.js edit, no Setup-tab module list, no extra npm install. When the Sterfive palette is loaded, it publishes a small bootstrap helper at global.get("sterfive") at startup. Your Function nodes reach into it instead of importing.

This page shows the recommended pattern: a single Function node that calls bootstrap.bootstrapServer({...}). The helper owns the server lifecycle — idempotent build, one-time address-space population, automatic teardown on SIGINT/SIGTERM/process exit — so your Function body stays focused on what your server actually exposes.

What you will build

  • A flow with two Function nodes: one that boots the server (idempotently — repeated injects reuse the running handle), and one that updates a variable's value on every input.
  • A dynamic variable that reflects msg.value in real time, without restarting the server.
  • A clean shutdown when Node-RED stops or the flow is redeployed.

Prerequisites

  • Node-RED running with the Sterfive OPC UA palette installed.
  • That's it. No external module to declare in the Setup tab; no functionGlobalContext entry. The palette wires global.get("sterfive") for you.

This is also why the pattern works under functionExternalModules: false.

Step 1 — Place the nodes

  1. Inject (set to "inject once after deploy"): may carry an optional msg.config = { port, endpoint }.
  2. Function named Boot Server: receives the init message, calls bootstrapServer.
  3. Status / Debug wired after Boot Server.
  4. A second Inject (or any input) emitting msg.value.
  5. Function named Update Value: receives value messages and writes via setValueFromSource.
[Inject once] → [Boot Server] → [Debug]
[Inject value] → [Update Value]

The two Function nodes share the live UA variable through the flow context.

Step 2 — The Boot Function

Paste this into the Boot Server Function node body:

const sterfive = global.get("sterfive");
if (!sterfive) {
node.error("global.get('sterfive') is not set — is the Sterfive OPC UA palette loaded?");
} else {
const { bootstrap, opcua } = sterfive;
const { bootstrapServer } = bootstrap;

const cfg = msg.config || {};

const handle = await bootstrapServer({
port: cfg.port ?? 4840,
endpoint: cfg.endpoint || "node-red-server",
nodesets: ["standard"],
onPopulate: (addressSpace, exposed) => {
const ns = addressSpace.getOwnNamespace();
exposed.myVariable = ns.addVariable({
organizedBy: "RootFolder",
nodeId: "s=MyDynamicVariable",
browseName: "MyDynamicVariable",
dataType: "Double",
value: { dataType: opcua.DataType.Double, value: 0.0 },
});
},
});

flow.set("$myVariable", handle.exposed.myVariable);
flow.set("$opcuaHandle", handle);

node.send({ payload: `OPC UA Server running at ${handle.server.getEndpointUrl()}` });
}

How it works:

  • global.get("sterfive") is the single entry point. Destructure { bootstrap, opcua } from it: bootstrap carries the helpers (bootstrapServer, getServerInfo, shutdownAllServers, ...) and opcua is the entire node-opcua namespace re-exported for convenience (so opcua.DataType.Double, opcua.SecurityPolicy.*, etc. are all available without a require).
  • bootstrapServer(config) is idempotent. The helper hashes the config; if you call it again with the same config while a server is up, it returns the existing handle. If the config changes, the previous server is torn down before the new one is built.
  • onPopulate(addressSpace, exposed) runs exactly once — only when a server is built fresh, not when an existing handle is adopted on same-config redeploy. Stash anything you'll need later in exposed; it surfaces on handle.exposed.
  • nodesets: ["standard"] accepts built-in names ("standard", "di", "autoId", "machinery", "ia", ...) and absolute paths to .NodeSet2.xml files.
  • The helper registers SIGINT / SIGTERM / exit handlers on first call, so process shutdown tears the server down automatically. You do not need to write a node.on("close", ...) handler for the basic case — see Stopping the server explicitly if you want explicit teardown from a flow.

Step 3 — The Update Function

Paste this into the Update Value Function node:

const sterfive = global.get("sterfive");
if (!sterfive) {
node.error("global.get('sterfive') is not set — is the Sterfive OPC UA palette loaded?");
} else {
const { opcua } = sterfive;
const variable = flow.get("$myVariable");

if (!variable) {
node.warn("OPC UA server is not booted yet — drop or buffer the message");
} else {
variable.setValueFromSource({
dataType: opcua.DataType.Double,
value: Number(msg.value),
});
}
}

This function does no I/O and no allocation beyond the Variant. It is safe to call thousands of times per second and never restarts the server.

Step 4 — Deploy and test

  1. Click Deploy.
  2. The first Inject fires automatically (since you set "inject once after deploy"); the Debug node should show OPC UA Server running at opc.tcp://....
  3. Trigger the value Inject repeatedly with different msg.value numbers.
  4. Connect any OPC UA client (UaExpert, the opcua-commander CLI, or another Node-RED flow) to the endpoint URL and read MyDynamicVariable. The value should follow your injects in real time.

Step 5 — Verify the lifecycle

These are the behaviours the bootstrap helper guarantees, and what to check:

BehaviourHow to verify
Server is created onceRepeated boot injects with the same config return the same handle.server; onPopulate is not re-invoked.
Updates do not restart the serverThe endpoint URL stays the same across many value injects. A connected client keeps its session alive.
Redeploy releases the portA config change (or forceRebuild: true) tears the previous server down before binding the new one.
Process exit shuts the server downStop Node-RED with Ctrl+C; the SIGINT/SIGTERM/exit hooks call shutdownAllServers and release the port.

The handle API

bootstrapServer(config) returns a handle object:

Field / methodPurpose
handle.serverThe underlying OPCUAServer instance — escape hatch for advanced reads (e.g. getServerInfo).
handle.addressSpaceConvenience pointer; same as server.engine.addressSpace.
handle.exposedThe bag your onPopulate callback wrote into. Empty {} when an existing handle was adopted.
handle.isRunning()true until shutdown() resolves or the process tears down.
handle.shutdown(timeoutMs)Explicit teardown. Used by the Stop Server and Server-Info Function nodes.

Next steps

Further reading

For more tips and examples, see the Sterfive book node-opcua by example.