> For the complete documentation index, see [llms.txt](https://docs.roboflow.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.roboflow.com/deployment/self-hosted/enterprise/deployment-manager/services/opc-ua-server.md).

# OPC UA Server

OPC UA (Open Platform Communications Unified Architecture) is an industrial communication protocol for exchanging data between devices, PLCs, and SCADA systems. The OPC UA Server is an edge container service that publishes tags on the device so industrial clients can read inference results and write values back.

You define the tags, folders, and server settings from the Deployment Manager Configuration tab, then point your OPC UA client at the device.

{% hint style="info" %}
OPC UA Server is available exclusively for Enterprise customers. [Contact the Roboflow sales team](https://roboflow.com/sales) to learn more.
{% endhint %}

## Connection Details

Replace `<device-ip>` with the IP address shown on the device page in Deployment Manager.

<table data-search="false"><thead><tr><th>Purpose</th><th>Address</th></tr></thead><tbody><tr><td>OPC UA endpoint</td><td><code>opc.tcp://&#x3C;device-ip>:4840/opcua/server</code></td></tr><tr><td>Web UI and REST API</td><td><code>http://&#x3C;device-ip>:8092</code></td></tr></tbody></table>

<table data-search="false"><thead><tr><th>Port</th><th>Protocol</th><th>Purpose</th></tr></thead><tbody><tr><td>4840</td><td>OPC UA Binary</td><td>Industrial client connections (PLCs, SCADA)</td></tr><tr><td>8092</td><td>HTTP</td><td>Web UI, REST API, documentation</td></tr></tbody></table>

## Configuration Source

The "Configuration Source" toggle at the top of the Configure modal controls where the tag and folder configuration comes from. Only one source is ever active on the device at a time.

<table data-search="false"><thead><tr><th>Source</th><th>Stored in</th><th>Use it when</th></tr></thead><tbody><tr><td>"Web UI"</td><td><code>OPCUA_CONFIG</code></td><td>You want to define tags, folders, and server settings inline in the modal. This is the default.</td></tr><tr><td>"File"</td><td><code>OPCUA_CONFIG_FILE</code></td><td>The configuration is too large for an environment variable, such as a deployment with thousands of tags. Point the server at a path on the device (ex: <code>/data/opcua-config.json</code>).</td></tr></tbody></table>

In "File" mode the server validates at startup that the file exists, is readable, and is not empty, and fails to start with an explicit log entry if any of that is untrue. `OPCUA_CONFIG_FILE` takes precedence over `OPCUA_CONFIG`.

Switching sources keeps both the inline values and the file path while the modal is open. On save, the source you did not pick is cleared, so exactly one of the two variables is set. In "File" mode the service card shows the configured file path in place of the inline tag list.

## Server Settings

<table data-search="false"><thead><tr><th>Setting</th><th>Description</th></tr></thead><tbody><tr><td>"Server Name"</td><td>Human-readable name for the server instance, shown to clients browsing the network.</td></tr><tr><td>"Namespace URI"</td><td>Unique URI identifying the server namespace that tags are registered under. Defaults to <code>http://opcua.roboflow.run</code>.</td></tr><tr><td>"Max Sessions"</td><td>How many OPC UA clients may be connected at once. Defaults to <code>200</code>.</td></tr></tbody></table>

Every client holds one session, including each inference pod writing tags and each SCADA connection. A session belonging to a client that crashed is only released when the server times it out, so a busy site can hold more sessions than it has live clients. Once the cap is reached, further clients are refused with `BadTooManySessions` and their tags stop updating. Raise the value if a site runs more concurrent clients than the default. Setting it in the modal writes it into the server config, and `OPCUA_MAX_SESSIONS` can override it per device for hand-managed deployments.

## Folders and Tags

Folders organize tags into a hierarchy. Each folder has a name and an optional description, and appears as a node in the OPC UA address space so clients can browse and discover tags. A tag can belong to a folder or sit at the root level.

Tags define the data points the server exposes. Each tag has these fields:

<table data-search="false"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>"Display Name"</td><td>Human-readable name shown to OPC UA clients.</td></tr><tr><td>"Browse Name"</td><td>Programmatic identifier used in the address space. Generated from the display name.</td></tr><tr><td>"Data Type"</td><td>The type of value the tag holds.</td></tr><tr><td>"Access Level"</td><td>Whether the tag is <code>ReadWrite</code> or <code>ReadOnly</code>.</td></tr><tr><td>"Folder"</td><td>Which folder the tag belongs to, or Root if unassigned.</td></tr><tr><td>"Initial Value"</td><td>Starting value when the server starts. Validated against the data type.</td></tr><tr><td>"Description"</td><td>Optional label describing what the tag is for.</td></tr></tbody></table>

### Data Types

<table data-search="false"><thead><tr><th>Type</th><th>Description</th><th>Example</th></tr></thead><tbody><tr><td><code>Boolean</code></td><td>True or false</td><td><code>true</code></td></tr><tr><td><code>Int32</code></td><td>32-bit signed integer</td><td><code>42</code></td></tr><tr><td><code>Float</code></td><td>32-bit floating point</td><td><code>3.14</code></td></tr><tr><td><code>Double</code></td><td>64-bit floating point</td><td><code>3.14159265359</code></td></tr><tr><td><code>String</code></td><td>UTF-8 text</td><td><code>Running</code></td></tr><tr><td><code>DateTime</code></td><td>ISO 8601 timestamp</td><td><code>2024-01-15T10:30:00Z</code></td></tr></tbody></table>

These are the types the Configure modal offers. A hand-written configuration file may also use `Int16`, `UInt16`, and `UInt32`, which the server accepts.

### Access Levels

`ReadWrite` tags can be read and written by OPC UA clients as well as the REST API, CLI, and web UI. Use them for control outputs, setpoints, and user-adjustable values.

`ReadOnly` tags reject OPC UA client writes with `BadNotWritable`. The REST API, CLI, and web UI can still update the value regardless of access level, so use `ReadOnly` for sensor values, calculated outputs, and system status that only the device should produce.

### Optional Constraints

Numeric types support "Min Value" and "Max Value" to restrict the allowed range, plus "Engineering Units" for a descriptive unit label (ex: °C, PSI, RPM). String types support "Max Length" to cap the character count.

## Authentication

The server allows anonymous connections by default, using SecurityPolicy None. Turn on "Require authentication" in the Configure modal to require a username and password from every client.

When the toggle is on you must supply both a username and a password. The password is hashed with bcrypt before it is stored, so the original is never saved and cannot be retrieved later. To change the password on an existing server, enter a new one in the Configure modal, or leave the field blank to keep the current password. Turning the toggle off restores anonymous access.

The credentials are stored as two environment variables, which must be set together or both left unset:

<table data-search="false"><thead><tr><th>Variable</th><th>Description</th></tr></thead><tbody><tr><td><code>OPCUA_USERNAME</code></td><td>Plain-text username for client authentication.</td></tr><tr><td><code>OPCUA_PASSWORD_HASH</code></td><td>Bcrypt hash of the password at salt rounds 10, starting with <code>$2b$10$</code>.</td></tr></tbody></table>

{% hint style="warning" %}
Set credentials through the Configure modal rather than by editing the environment variables. The modal generates the bcrypt hash for you, and setting `OPCUA_PASSWORD_HASH` by hand requires producing a valid hash externally.
{% endhint %}

## Web Interface

The server hosts a web interface at `http://<device-ip>:8092`.

<table data-search="false"><thead><tr><th>Page</th><th>Path</th><th>Contents</th></tr></thead><tbody><tr><td>Dashboard</td><td><code>/</code></td><td>All configured tags with current values, refreshed every 2 seconds. Any tag's value can be edited through an edit dialog, regardless of access level.</td></tr><tr><td>Documentation</td><td><code>/docs.html</code></td><td>Server guide, configuration format reference, and troubleshooting notes.</td></tr><tr><td>API Explorer</td><td><code>/api/docs</code></td><td>Interactive Swagger UI for testing endpoints and viewing request and response schemas.</td></tr></tbody></table>

## HTTP API

The REST API gives programmatic access to tag values and server information. All endpoints return JSON. The base URL is `http://<device-ip>:8092/api`. See [Services](/deployment/self-hosted/enterprise/deployment-manager/services.md#using-the-apis) for the rules shared by all on-device service APIs.

{% hint style="warning" %}
The HTTP API is unauthenticated. `OPCUA_USERNAME` and `OPCUA_PASSWORD_HASH` protect the OPC UA binary endpoint on port 4840, not port 8092. Anything that can reach port 8092 can write any tag.
{% endhint %}

Tag structure is immutable at runtime. Tags and folders come from the `OPCUA_CONFIG` environment variable, or the file named by `OPCUA_CONFIG_FILE` for large configurations, and cannot be created, changed, or deleted through this API. Only values can be written.

### Read and Write Tag Values

`accessLevel` governs the OPC UA protocol, not this API. A tag marked `ReadOnly` rejects writes from OPC UA clients with `BadNotWritable` but remains writable here, which is how a pipeline publishes a sensor value that industrial clients should only consume.

```bash
curl http://<device-ip>:8092/api/tags

curl -X PUT http://<device-ip>:8092/api/tags/tag_temperature/value \
  -H "Content-Type: application/json" \
  -d '{"value": 25.5}'
```

{% openapi src="/files/uORKup3XjyYVoDzxt3Kp" path="/tags" method="get" %}
[edge-opcua-server.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-21a215712d389ded373872b7a4e970e80be2c5fa%2Fedge-opcua-server.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/uORKup3XjyYVoDzxt3Kp" path="/tags/{id}" method="get" %}
[edge-opcua-server.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-21a215712d389ded373872b7a4e970e80be2c5fa%2Fedge-opcua-server.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/uORKup3XjyYVoDzxt3Kp" path="/tags/{id}/value" method="put" %}
[edge-opcua-server.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-21a215712d389ded373872b7a4e970e80be2c5fa%2Fedge-opcua-server.yaml?alt=media)
{% endopenapi %}

A write is rejected with `400` when the value is missing, the type does not match the tag's `dataType`, or it falls outside a configured `minValue`, `maxValue`, or `maxLength`. Writing to a tag that does not exist also returns `400`, not `404`. `GET /tags/{id}` does return `404`, so use it if you need to distinguish an unknown tag from a bad value.

### Configuration Endpoints

Read-only views of the running configuration, useful for discovering tag IDs and their constraints before writing.

{% openapi src="/files/uORKup3XjyYVoDzxt3Kp" path="/config" method="get" %}
[edge-opcua-server.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-21a215712d389ded373872b7a4e970e80be2c5fa%2Fedge-opcua-server.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/uORKup3XjyYVoDzxt3Kp" path="/config/folders" method="get" %}
[edge-opcua-server.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-21a215712d389ded373872b7a4e970e80be2c5fa%2Fedge-opcua-server.yaml?alt=media)
{% endopenapi %}

### Sessions and Health

Once `maxSessions` is reached, further clients are refused with `BadTooManySessions`, so compare `sessionCount` against `maxSessions` for headroom.

A `200` from `/subscriptions` with a zeroed session list and `maxSessions` omitted means the OPC UA server is not running. Treat it as an outage rather than a warm-up: if the endpoint answers at all, startup has already finished or failed. Confirm with `/health`, which reports `status: "unhealthy"` and `opcuaServer: "stopped"`.

`maxSessions` is omitted rather than zeroed in that case so a consumer computing `maxSessions - sessionCount` does not read an unknown cap as a full one.

{% openapi src="/files/uORKup3XjyYVoDzxt3Kp" path="/subscriptions" method="get" %}
[edge-opcua-server.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-21a215712d389ded373872b7a4e970e80be2c5fa%2Fedge-opcua-server.yaml?alt=media)
{% endopenapi %}

{% hint style="info" %}
`/subscriptions` is missing from the spec the device serves at `/api/openapi.json`. The route is live regardless, and it is documented here.
{% endhint %}

`/health` returns `200` whether or not the OPC UA server is running. Check `status` and `opcuaServer` rather than the status code.

{% openapi src="/files/uORKup3XjyYVoDzxt3Kp" path="/health" method="get" %}
[edge-opcua-server.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-21a215712d389ded373872b7a4e970e80be2c5fa%2Fedge-opcua-server.yaml?alt=media)
{% endopenapi %}

## CLI

The `opcua-cli` tool gives terminal access to the server for debugging, scripting, and quick tag operations when the web UI is not reachable.

```bash
docker exec -it opcua-server opcua-cli
```

```bash
docker exec opcua-server opcua-cli <command>
```

<table data-search="false"><thead><tr><th>Command</th><th>Description</th></tr></thead><tbody><tr><td><code>list</code>, <code>ls</code></td><td>List all tags with current values</td></tr><tr><td><code>read &#x3C;tag></code></td><td>Read a specific tag value</td></tr><tr><td><code>write &#x3C;tag> &#x3C;value></code></td><td>Write a value to a tag</td></tr><tr><td><code>status</code></td><td>Show server health and uptime</td></tr><tr><td><code>clients</code></td><td>List connected OPC UA clients</td></tr><tr><td><code>clients --detailed</code></td><td>List clients with subscription details</td></tr><tr><td><code>export</code></td><td>Export the configuration as JSON</td></tr></tbody></table>

```bash
docker exec opcua-server opcua-cli read Temperature
docker exec opcua-server opcua-cli write Temperature 25.5
docker exec opcua-server opcua-cli clients --detailed
```

## Logging

Set `LOG_LEVEL` on the service to control verbosity. See [Update Device Configuration](/deployment/self-hosted/enterprise/deployment-manager/making-changes/update-device-configuration.md) for where to edit service environment variables, and [View Device Logs](/deployment/self-hosted/enterprise/deployment-manager/monitoring/view-device-logs.md) for reading the output.

<table data-search="false"><thead><tr><th>Level</th><th>Description</th></tr></thead><tbody><tr><td><code>DEBUG</code></td><td>Detailed diagnostic information</td></tr><tr><td><code>INFO</code></td><td>General operational messages (default)</td></tr><tr><td><code>WARN</code></td><td>Potential issues that do not stop operation</td></tr><tr><td><code>ERROR</code></td><td>Errors that affect functionality</td></tr></tbody></table>

Values are case-insensitive, and an unrecognized value falls back to `INFO`.
