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.valuein 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
functionGlobalContextentry. The palette wiresglobal.get("sterfive")for you.
This is also why the pattern works under functionExternalModules: false.
Step 1 — Place the nodes
- Inject (set to "inject once after deploy"): may carry an optional
msg.config = { port, endpoint }. - Function named Boot Server: receives the init message, calls
bootstrapServer. - Status / Debug wired after Boot Server.
- A second Inject (or any input) emitting
msg.value. - 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:bootstrapcarries the helpers (bootstrapServer,getServerInfo,shutdownAllServers, ...) andopcuais the entirenode-opcuanamespace re-exported for convenience (soopcua.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 inexposed; it surfaces onhandle.exposed.nodesets: ["standard"]accepts built-in names ("standard","di","autoId","machinery","ia", ...) and absolute paths to.NodeSet2.xmlfiles.- The helper registers
SIGINT/SIGTERM/exithandlers on first call, so process shutdown tears the server down automatically. You do not need to write anode.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
- Click Deploy.
- The first Inject fires automatically (since you set "inject once after deploy"); the Debug node should show
OPC UA Server running at opc.tcp://.... - Trigger the value Inject repeatedly with different
msg.valuenumbers. - 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:
| Behaviour | How to verify |
|---|---|
| Server is created once | Repeated boot injects with the same config return the same handle.server; onPopulate is not re-invoked. |
| Updates do not restart the server | The endpoint URL stays the same across many value injects. A connected client keeps its session alive. |
| Redeploy releases the port | A config change (or forceRebuild: true) tears the previous server down before binding the new one. |
| Process exit shuts the server down | Stop 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 / method | Purpose |
|---|---|
handle.server | The underlying OPCUAServer instance — escape hatch for advanced reads (e.g. getServerInfo). |
handle.addressSpace | Convenience pointer; same as server.engine.addressSpace. |
handle.exposed | The 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
- Server lifecycle — what the helper does for you, in detail.
- Custom objects and methods
- User authentication
- Security policies and certificates
- A structured namespace layout
- Loading a companion specification (RFID)
- Scaling and limits
- Stopping the server explicitly
- Inspecting the running server
Further reading
For more tips and examples, see the Sterfive book node-opcua by example.