> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# OPC UA Server Route

> Expose MQTT topics as OPC UA nodes so SCADA systems, PLCs, and OPC UA clients connect to your Coreflux broker

## OPC UA Server

> [!NOTE]
>
> This page documents the **server** route (`OPCUA_SERVER`)—the broker **hosts** an OPC UA server and exposes MQTT topics as nodes. External clients connect to the broker. To connect the broker **to** a remote OPC UA device instead, see [OPC UA Client](./opcua-client).

The `OPCUA_SERVER` route makes your broker **host** an OPC UA server. External clients—SCADA, UaExpert, PLCs—connect **to the broker** and see your MQTT topics as OPC UA nodes.

Use `OPCUA` when you need to **read from** a device. Use `OPCUA_SERVER` when OT systems need to **browse and write** values that live on your MQTT bus.

> [!TIP]
>
> **Like turning your broker into a shop window.** MQTT messages update what's on display (node values), and customers (OPC UA clients) can look—and, for writable tags, change items that publish back to MQTT.

## When to Use This Route

* **SCADA integration** — Let a SCADA or HMI browse broker data without custom drivers.
* **OT/IT bridge** — Expose IoT sensor streams to plant-floor tools that speak OPC UA.
* **Bidirectional control** — Writable tags let clients adjust setpoints; the broker publishes the new value to MQTT.
* **Dynamic discovery** — Wildcard topic mappings create nodes automatically as matching topics first publish.

***

The sections below cover data flow, configuration, persistence, and production patterns.

## Quick Start

**Expose static tags**

Map MQTT topics to fixed OPC UA nodes. Each `ADD TAG` creates one node; publishing to `{SOURCE_TOPIC}/{TagName}` updates it.

```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
    ADD MAPPING Line1
        WITH SOURCE_TOPIC "factory/line1"
        ADD TAG Temperature
            WITH DATA_TYPE "FLOAT"
        ADD TAG Counter
            WITH DATA_TYPE "INT32"
```

* Publish `23.5` to `factory/line1/Temperature` → node `ns=2;s=PlantServer/Temperature` reads `23.5`.
* Publish `1042` to `factory/line1/Counter` → node `ns=2;s=PlantServer/Counter` reads `1042`.

**Writable setpoints**

Allow OPC UA clients to write a node; the broker publishes the value back to MQTT on the same topic, retained.

```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
    ADD MAPPING Line1
        WITH SOURCE_TOPIC "factory/line1"
        ADD TAG Setpoint
            WITH DATA_TYPE "FLOAT"
            WITH WRITABLE true
            WITH DECIMAL_PLACES 1
            WITH MIN_VALUE 0
            WITH MAX_VALUE 100
```

MQTT publishes set the node. A client writing `72.4` to the Setpoint node publishes `72.4` to `factory/line1/Setpoint` (clamped and rounded per tag settings).

**Dynamic wildcard topics**

Omit `ADD TAG` and use an MQTT wildcard in `SOURCE_TOPIC`. Nodes appear automatically when matching topics first publish—see [Dynamic topic handling](#dynamic-topic-handling).

```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DynamicSensors WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
    ADD MAPPING Sensors
        WITH SOURCE_TOPIC "sensors/+/value"
```

First publish to `sensors/press1/value` creates node `ns=2;s=sensors/press1/value`. First publish to `sensors/tank2/value` creates another node. No route edit required.

**Secure production server**

Encrypt traffic, require credentials, and combine static tags with dynamic discovery:

```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SecurePlantServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH ROOT_FOLDER "Plant"
        WITH NAMESPACE_URI "urn:acme:plant"
        WITH USE_SECURITY true
        WITH SECURITY_POLICY 3
        WITH MESSAGE_SECURITY 2
        WITH ALLOW_ANONYMOUS false
        WITH USERNAME GET ENV "OPCUA_USER"
        WITH PASSWORD GET SECRET "OPCUA_PASSWORD"
    ADD MAPPING Line1
        WITH SOURCE_TOPIC "factory/line1"
        ADD TAG Setpoint
            WITH DATA_TYPE "FLOAT"
            WITH WRITABLE true
            WITH DECIMAL_PLACES 1
            WITH MIN_VALUE 0
            WITH MAX_VALUE 100
    ADD MAPPING Sensors
        WITH SOURCE_TOPIC "sensors/+/value"
```

Clients connect with `opc.tcp://<broker-host>:4840` using Basic256Sha256 and SignAndEncrypt. Store credentials with `GET ENV` / `GET SECRET`—never hard-code passwords.

**Crash-resilient setpoints**

Persist client-written setpoints across broker restarts:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ResilientServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH PERSISTENCE true
    ADD MAPPING Control
        WITH SOURCE_TOPIC "plant/line1"
        ADD TAG Setpoint
            WITH DATA_TYPE "FLOAT"
            WITH WRITABLE true
            WITH DECIMAL_PLACES 1
            WITH MIN_VALUE 0
            WITH MAX_VALUE 100
        ADD TAG Temperature
            WITH DATA_TYPE "FLOAT"
```

After a restart, a client reading `Setpoint` gets the last value it wrote instead of `BadWaitingForInitialData`. `Temperature` refreshes from the live MQTT source, not the snapshot.

***

## How Data Flows

```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
graph LR
    subgraph mqtt [MQTT]
        Topics[Topics]
    end

    subgraph broker [Coreflux Broker]
        Route[OPCUA_SERVER Route]
        Nodes[OPC UA Nodes]
    end

    subgraph ot [OT Clients]
        Client[SCADA / UaExpert / PLC]
    end

    Topics -->|publish sets value| Route
    Route --> Nodes
    Nodes -->|browse / read| Client
    Client -->|write writable tag| Route
    Route -->|publish retained| Topics
```

| Direction | Behavior |
| - | - |
| **MQTT → node** | Each tag binds to an MQTT topic via `SOURCE_TOPIC`. A publish on that topic sets the node value. The MQTT payload is authoritative—no scaling or rounding is applied inbound. Retained values present before the route starts are seeded at startup. |
| **node → MQTT** | When a tag has `WITH WRITABLE true`, an external client writing the node publishes the new value back to the **same** `SOURCE_TOPIC`, retained. `FLOAT` and `DOUBLE` values are rounded to `DECIMAL_PLACES` (default 2) and clamped by `MIN_VALUE` / `MAX_VALUE`. Client writes do not loop back into duplicate node updates. |
| **Wildcard mappings** | Wildcard `SOURCE_TOPIC` patterns create nodes on first publish. See [Dynamic topic handling](#dynamic-topic-handling). |

***

## Server Configuration

All settings live in the `OPCUA_SERVER_CONFIG` block. Store credentials with `GET ENV` or `GET SECRET`.

The example below shows a production-oriented config: custom identity, encrypted endpoint, username/password auth, and a persistent certificate store. Clients connect at `opc.tcp://<broker-host>:4840/coreflux`.

```lot wrap focus={2-16} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantOpcServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH ENDPOINT_PATH "/coreflux"
        WITH APPLICATION_NAME "Coreflux Plant Server"
        WITH APPLICATION_URI "urn:plant.example.com:Coreflux:PlantOpcServer"
        WITH NAMESPACE_URI "urn:plant.example.com:opcua"
        WITH ROOT_FOLDER "Plant"
        WITH PKI_DIR "/data/opcua-pki"
        WITH USE_SECURITY true
        WITH SECURITY_POLICY 3
        WITH MESSAGE_SECURITY 2
        WITH ALLOW_ANONYMOUS false
        WITH USERNAME GET ENV "OPCUA_USER"
        WITH PASSWORD GET SECRET "OPCUA_PASSWORD"
        WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES false
    ADD MAPPING Tags
        WITH SOURCE_TOPIC "plant/tags"
        ADD TAG Status
            WITH DATA_TYPE "INT32"
```

### Connection and identity

**SERVER\_PORT** `integer` (default: 4840)

TCP port the server listens on (`opc.tcp`). Each server route claims its port process-wide—two routes cannot share the same port.

**ENDPOINT\_PATH** `string`

Optional path appended to the base address, for example `/coreflux`. Default: empty.

**APPLICATION\_NAME** `string` (default: CorefluxBroker\_OpcUaServer)

Application display name and certificate common name (CN).

**APPLICATION\_URI** `string` (default: urn:\{host}:Coreflux:\{route})

Application URI. Must match the certificate SAN URI. Changing this regenerates the certificate.

**NAMESPACE\_URI** `string` (default: urn:coreflux:opcua:\{route})

Namespace URI for exposed nodes.

**ROOT\_FOLDER** `string` (default: route name)

Root folder under `Objects` where tags are organized.

**PKI\_DIR** `string`

Directory for own, issuer, trusted, and rejected certificate stores. Defaults to the OPC Foundation PKI layout. In Docker, mount this path as a volume so certificates persist across restarts. See [Certificate Management](#certificate-management).

### Security

**USE\_SECURITY** `boolean` (default: true)

Offer a signed or encrypted endpoint.

> [!NOTE]
>
> This defaults to `true` to comply with OPC Foundation specifications, which require secure endpoints. For testing purposes, you might need to set it to `false`.

**SECURITY\_POLICY** `integer` (default: 0)

Security policy offered when `USE_SECURITY` is true:

| Value | Policy |
| - | - |
| `0` | None |
| `3` | Basic256Sha256 |
| `4` | Aes128\_Sha256\_RsaOaep |
| `5` | Aes256\_Sha256\_RsaPss |

**MESSAGE\_SECURITY** `integer` (default: 0)

Message security mode. Requires `SECURITY_POLICY` ≥ 1 when using Sign or SignAndEncrypt.

| Value | Mode |
| - | - |
| `0` | None |
| `1` | Sign |
| `2` | SignAndEncrypt |

**ALLOW\_NO\_SECURITY** `boolean` (default: false)

When `USE_SECURITY` is true, also offer a None/None endpoint. Use for testing only.

### Authentication

**ALLOW\_ANONYMOUS** `boolean` (default: true)

Offer anonymous login. Set to `false` for production and configure `USERNAME`/`PASSWORD` or `ALLOW_CERTIFICATE_AUTH`.

**USERNAME** `string`

Username for password-based login. Use `GET ENV` or `GET SECRET`.

**PASSWORD** `string`

Password for password-based login. Use `GET ENV` or `GET SECRET`.

**ALLOW\_CERTIFICATE\_AUTH** `boolean` (default: false)

Offer X.509 certificate user authentication.

**AUTO\_ACCEPT\_UNTRUSTED\_CERTIFICATES** `boolean` (default: false)

Automatically trust connecting client certificates. When false, untrusted client certs are placed in the `rejected` store.

**PERSISTENCE** `boolean` (default: false)

Persist served node values across broker restarts. See [Data persistence & recovery](#data-persistence--recovery).

> [!NOTE]
>
> An application instance certificate is auto-generated (self-signed) on first start under `PKI_DIR/own`. If you change `APPLICATION_URI`, the certificate is regenerated to match.

***

## Tag and Mapping Configuration

Tags reuse the same vocabulary as the [OPC UA client route](./opcua-client), but several settings carry different meaning on the server side.

### Server vs. client semantics

| Setting | Client (`OPCUA`) | Server (`OPCUA_SERVER`) |
| - | - | - |
| `SOURCE_TOPIC` | Topic where read values are **published** | **Bidirectional binding** — MQTT sets the node; client writes publish back here |
| `WRITABLE` | Broker may write the remote device | External clients may write the node |
| `DESTINATION_TOPIC` | Write-command topic | Not used on server routes |
| `ADDRESS` | Required remote NodeId | Optional explicit NodeId; default `s={ROOT_FOLDER}/{TagName}` |
| `EVERY` | Poll interval | Ignored — updates are event-driven from MQTT |
| `QUERY` events | On-demand read/write | Not supported |
| `SCALING` / `OFFSET` / `DEADBAND` | Applied to reads from the device | Not applied inbound — MQTT payload is authoritative |
| `DECIMAL_PLACES` / `MIN_VALUE` / `MAX_VALUE` | Applied to reads | Applied when a client write is published to MQTT |
| `STORE TO` / triggers | Supported on client routes | Not supported |

On server routes, route status **Connected** means the endpoint is **listening**—not that a remote session is established.

### Explicit NodeId

By default, a tag named `Temperature` under route `PlantServer` with `ROOT_FOLDER` defaulting to the route name maps to `ns=2;s=PlantServer/Temperature`. Override with `ADDRESS`:

```lot wrap focus={6,7} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
    ADD MAPPING Line1
        WITH SOURCE_TOPIC "factory/line1"
        ADD TAG Temperature
            WITH ADDRESS "ns=2;s=Custom/Line1/Temp"
            WITH DATA_TYPE "FLOAT"
```

### Writable tag parameters

For writable tags, configure outbound formatting the same way as on the client route:

**WRITABLE** `boolean` (default: false)

Allow OPC UA clients to write this node. Writes publish to the tag's `SOURCE_TOPIC`.

**DECIMAL\_PLACES** `integer` (default: 2)

Decimal places applied to `FLOAT`/`DOUBLE` values when a client write is published to MQTT.

**MIN\_VALUE** `double`

Minimum value after a client write. Values below this are clamped.

**MAX\_VALUE** `double`

Maximum value after a client write. Values above this are clamped.

**PERSISTENCE** `boolean`

Per-tag override of the route persistence default. Precedence: tag → mapping → route. See [Data persistence & recovery](#data-persistence--recovery).

### Supported data types

Use the same `DATA_TYPE` values as the [OPC UA client route](./opcua-client)—for example `BOOLEAN`, `INT32`, `FLOAT`, `DOUBLE`, and `STRING`. Tags can also expose one-dimensional **arrays** or **struct objects**—see [Array and struct nodes](#array--struct-nodes). For statically defined scalar tags, set `DATA_TYPE` explicitly. Dynamic nodes infer type from the first MQTT payload (see below).

> [!NOTE]
>
> **String sizing:** `STRING` nodes are natively variable-length in OPC UA. The `OPCUA_SERVER` route does not accept `LENGTH` or `STRING_SIZE`—there is no string-size parameter for scalar string tags. For array tags, `ARRAY_SIZE` sets the **node dimension** only (how many elements the array node exposes), not a string buffer width.

***

## Dynamic Topic Handling

Wildcard mappings let you expose many MQTT topics without listing each one as an `ADD TAG`. When a topic matches the pattern for the first time, the route creates a new OPC UA node automatically.

### How it works

1. Define a mapping with a wildcard `SOURCE_TOPIC`—for example `sensors/+/value` or `plant/#`.
2. Do **not** add `ADD TAG` entries under that mapping. The wildcard pattern alone drives node creation.
3. On the **first publish** to a matching topic, the route creates an OPC UA node and sets its value from the payload.
4. Later publishes to the same topic update that node. High-frequency updates use an equality check so unchanged values do not spam change notifications.

Dynamic nodes are **read-only**. OPC UA clients cannot write them back to MQTT. Use static `ADD TAG` definitions with `WRITABLE true` when you need bidirectional control.

### Topic → node mapping

The NodeId string identifier mirrors the full MQTT topic path:

| MQTT topic (first publish) | OPC UA NodeId |
| - | - |
| `sensors/press1/value` | `ns=2;s=sensors/press1/value` |
| `sensors/tank2/value` | `ns=2;s=sensors/tank2/value` |
| `plant/line3/motor/rpm` | `ns=2;s=plant/line3/motor/rpm` |

Under `Objects`, folder nodes are created from topic segments so clients can browse a hierarchy that matches your MQTT layout—for example `sensors` → `press1` → `value`.

### Supported wildcards

| Pattern | Matches | Example |
| - | - | - |
| `+` (single level) | Exactly one topic segment | `sensors/+/value` matches `sensors/press1/value` but not `sensors/line1/temp/value` |
| `#` (multi-level) | Zero or more segments at the end | `plant/#` matches `plant/line1/temp` and `plant/line1/motor/rpm` |

> [!NOTE]
>
> Combine static and dynamic mappings in the same route. Static `ADD TAG` entries under a non-wildcard `SOURCE_TOPIC` behave as fixed nodes; wildcard mappings handle everything else that matches.

### Type inference

Dynamic nodes have no `DATA_TYPE` in the route definition. The broker infers the OPC UA type from the **first** payload and locks it for the lifetime of that node:

| First MQTT payload | Inferred OPC UA type |
| - | - |
| Numeric (`42`, `23.5`) | `Double` |
| `true` or `false` (case-insensitive) | `Boolean` |
| Anything else | `String` |

If a later publish cannot be converted to the pinned type, the node keeps its previous value and the broker logs an error. Publish a representative value first—do not send a placeholder string if you expect numeric updates later.

### Example

This mapping exposes every `sensors/<device>/value` topic as it appears on the bus:

```lot wrap focus={4-5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorGateway WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
    ADD MAPPING Sensors
        WITH SOURCE_TOPIC "sensors/+/value"
```

**Sequence:**

1. Route starts; no dynamic nodes exist yet.
2. Device publishes `45.2` to `sensors/press1/value` → node `ns=2;s=sensors/press1/value` appears with value `45.2` (`Double`).
3. Device publishes `false` to `sensors/alarm1/value` → node `ns=2;s=sensors/alarm1/value` appears with value `false` (`Boolean`).
4. UaExpert refresh shows both nodes under the `sensors` folder. Further publishes update values in place.

Retained messages already on the broker when the route starts are seeded the same way as for static tags—matching topics create nodes at startup.

### Dynamic node limits

> [!WARNING]
>
> Dynamic nodes are not removed while the route is running. They persist until the route is removed or the broker restarts. Plan wildcard patterns carefully in high-churn environments where topic names change frequently.

***

## Array & Struct Nodes

Scalar tags expose one value per node. Use an **array** tag when MQTT carries a list of values on one topic, or a **struct** tag when the payload is a JSON object with named fields. OPC UA clients then browse and write the same shape—no extra config block beyond the tag settings below.

### Array tags

An array tag turns a JSON list on MQTT into a single OPC UA array node. Publish a list; the client sees one node with multiple elements. With `WRITABLE true`, a client edit publishes the full list back to the same topic, retained.

**DATA\_TYPE** `string`

Set to `"ARRAY"` for list values.

**ARRAY\_ELEMENT\_TYPE** `string` (default: INT16)

Type of each element: `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, or `STRING`.

**ARRAY\_SIZE** `integer`

Number of elements in the array. Set this to match your payload length. If omitted, the dimension is unspecified.

Define the tag, then publish a JSON array to `{SOURCE_TOPIC}/{TagName}`:

```lot wrap focus={4-10} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
    ADD MAPPING Line1
        WITH SOURCE_TOPIC "factory/line1"
        ADD TAG Levels
            WITH DATA_TYPE "ARRAY"
            WITH ARRAY_ELEMENT_TYPE "DOUBLE"
            WITH ARRAY_SIZE 3
            WITH WRITABLE true
```

**What happens:**

* Publish `[1.5, 2.5, 3.5]` to `factory/line1/Levels` → node `ns=2;s=PlantServer/Levels` shows three values.
* Payloads wrapped as `{"value": [1.5, 2.5, 3.5]}` are accepted—the broker unwraps the list automatically.
* A client write to the array node publishes the updated list back to `factory/line1/Levels`.

### Struct tags

A struct tag maps a JSON object to a folder-like node with one child per field. Field names in MQTT must match the names you declare in `STRUCT_FIELD_DEFINITIONS`.

**DATA\_TYPE** `string`

Set to `"STRUCT"` for object payloads.

**STRUCT\_FIELD\_DEFINITIONS** `string`

Comma-separated field list in `name:type` form—for example `bRunning:BOOL,iRpm:INT,sState:STRING`. If you reuse a definition from an ADS or EtherNet/IP route (`name:offset:type:size`), the server uses the name and type only; offset and size are ignored.

```lot wrap focus={4-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
    ADD MAPPING Line1
        WITH SOURCE_TOPIC "factory/line1"
        ADD TAG Motor
            WITH DATA_TYPE "STRUCT"
            WITH WRITABLE true
            WITH STRUCT_FIELD_DEFINITIONS "bRunning:BOOL,iRpm:INT,sState:STRING"
```

**What happens:**

* Publish `{"bRunning": true, "iRpm": 1450, "sState": "RUN"}` to `factory/line1/Motor` → clients see `Motor.bRunning`, `Motor.iRpm`, and `Motor.sState` under the struct node.
* With `WRITABLE true`, a client writing one field to OPC-UA (for example `1600` to `Motor.iRpm`) merges into the current struct and publishes the **full** object back to MQTT `factory/line1/Motor`—for example `{"bRunning": true, "iRpm": 1600, "sState": "RUN"}`.
* Fields that were never set by MQTT and were not written by the client are left out of that publish.

> [!NOTE]
>
> `DECIMAL_PLACES`, `MIN_VALUE`, and `MAX_VALUE` apply to scalar writable tags only—not to individual struct fields.

***

## Security Examples

**Anonymous (development)**

For lab or trusted networks where encryption is not required:

```lot wrap focus={3-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DevServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH ALLOW_ANONYMOUS true
    ADD MAPPING Demo
        WITH SOURCE_TOPIC "demo/tags"
        ADD TAG Value
            WITH DATA_TYPE "FLOAT"
```

**Username and encryption**

Require credentials and encrypt traffic:

```lot wrap focus={4-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProdServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH USE_SECURITY true
        WITH SECURITY_POLICY 3
        WITH MESSAGE_SECURITY 2
        WITH ALLOW_ANONYMOUS false
        WITH USERNAME GET ENV "OPCUA_USER"
        WITH PASSWORD GET SECRET "OPCUA_PASSWORD"
    ADD MAPPING Tags
        WITH SOURCE_TOPIC "plant/tags"
        ADD TAG Setpoint
            WITH DATA_TYPE "FLOAT"
            WITH WRITABLE true
```

**Certificate user auth**

Accept X.509 certificates for client login:

```lot wrap focus={4-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CertServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH USE_SECURITY true
        WITH SECURITY_POLICY 3
        WITH MESSAGE_SECURITY 2
        WITH ALLOW_ANONYMOUS false
        WITH ALLOW_CERTIFICATE_AUTH true
        WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES false
    ADD MAPPING Tags
        WITH SOURCE_TOPIC "plant/tags"
        ADD TAG Status
            WITH DATA_TYPE "INT32"
```

***

## Certificate Management

A secured OPC UA server connection requires **two certificates that trust each other**: the broker's **application instance certificate** (identifies and secures the server) and each **client's certificate** (must be in the server's `trusted` store when using certificate authentication). For client-side trust workflows when the broker dials out to remote servers, see [OPC UA Client](./opcua-client#certificate-management).

### PKI directory layout

Both client and server routes store certificates in four folders under `PKI_DIR`:

| Folder | Holds | File form |
| - | - | - |
| `own` | This server's certificate (`own/certs`) and private key (`own/private`) | `*.der` + `*.pfx` |
| `issuer` | Trusted CA / issuer certificates for chain validation | `*.der` |
| `trusted` | Trusted client certificates (`trusted/certs`) | `*.der` |
| `rejected` | Client certificates seen during a handshake but not yet trusted | `*.der` |

### Where certificates live

When `PKI_DIR` is not set, it defaults to `%LocalApplicationData%/OPC Foundation/pki`:

| OS | Default path |
| - | - |
| macOS | `~/Library/Application Support/OPC Foundation/pki` |
| Linux | `$XDG_DATA_HOME/OPC Foundation/pki` or `~/.local/share/OPC Foundation/pki` |
| Windows | `%LOCALAPPDATA%\OPC Foundation\pki` |

Relocate the store to a path you control and mount it as a volume in Docker:

```lot wrap focus={4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD OPCUA_SERVER_CONFIG
    WITH SERVER_PORT 4840
    WITH PKI_DIR "/data/opcua/pki"
```

> [!WARNING]
>
> In Docker, the default PKI location is ephemeral—the broker regenerates its certificate on every restart and clients that trusted the previous one will reject the new one. **Always set `PKI_DIR` to a mounted volume.**

### Inspecting and exporting certificates

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# The server's own certificate and private key
ls "$PKI_DIR"/own/certs/*.der
ls "$PKI_DIR"/own/private/*.pfx

# Inspect subject, validity, and application URI SAN
openssl x509 -inform der -in "$PKI_DIR"/own/certs/<name>.der -noout -subject -dates -ext subjectAltName

# Export public cert for clients to trust
openssl x509 -inform der -in "$PKI_DIR"/own/certs/<name>.der -out server-public.pem
```

Give clients the **public** certificate (`own/certs/*.der`). The private key never leaves the broker.

### The server's own certificate

On first start the broker auto-generates a self-signed server certificate (`CN={APPLICATION_NAME}, O=Coreflux`; default `APPLICATION_NAME` is `CorefluxBroker_OpcUaServer`) in `PKI_DIR/own`. Its SAN URI matches `APPLICATION_URI` (default `urn:{host}:Coreflux:{route}`):

```lot wrap focus={4-8} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH APPLICATION_NAME "AcmePlantServer"
        WITH APPLICATION_URI "urn:acme:opcua:plant-server"
        WITH SECURITY_POLICY 3
        WITH MESSAGE_SECURITY 2
        WITH PKI_DIR "/data/opcua/pki"
```

* **Keep `APPLICATION_URI` and the certificate SAN in sync.** Strict clients refuse the handshake on a mismatch; changing `APPLICATION_URI` regenerates the certificate to match.
* Clients trust this server by adding `PKI_DIR/own/certs/<name>.der` to their trusted list—the same workflow as [trusting a server certificate on the client route](./opcua-client#trust-the-servers-certificate).

### Enable certificate user authentication

Offer the X.509 user token so clients authenticate with a certificate instead of—or alongside—username and password:

```lot wrap focus={6-7} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CertServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH USE_SECURITY true
        WITH ALLOW_ANONYMOUS false
        WITH ALLOW_CERTIFICATE_AUTH true
```

When `ALLOW_CERTIFICATE_AUTH true`, the application instance certificate doubles as the user identity—the client's certificate must be trusted and must carry a valid application URI SAN.

### Trust connecting client certificates

When a client connects with certificate authentication, its certificate must be in the server's trusted store:

**1. First connect**

The client's certificate lands in `PKI_DIR/rejected/certs`.

**2. Move to trusted**

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
mv "$PKI_DIR"/rejected/certs/<client-cert>.der "$PKI_DIR"/trusted/certs/
```

**3. Reconnect**

The client connects successfully.

For testing only, set `WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES true` to auto-trust every connecting client certificate.

### Supply your own server certificate

The server route has no `CLIENT_CERT_PATH`-style parameter. To run with a CA-issued certificate instead of the auto-generated one, place it in the `own` store **before first start**, with a subject matching `CN={APPLICATION_NAME}, O=Coreflux` and a SAN URI matching `APPLICATION_URI`:

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
mkdir -p "$PKI_DIR"/own/certs "$PKI_DIR"/own/private
openssl x509 -in server-cert.pem -outform der -out "$PKI_DIR"/own/certs/server.der
openssl pkcs12 -export -inkey server-key.pem -in server-cert.pem \
  -out "$PKI_DIR"/own/private/server.pfx -passout pass:
```

If a valid matching certificate is already present, the broker uses it instead of generating one.

### Rotation and renewal

* **Auto-generated certificates** — Delete files in `PKI_DIR/own/certs` and `PKI_DIR/own/private`, then restart the route; the broker regenerates.
* **Changing `APPLICATION_URI`** — Regenerates the certificate; re-trust it on every client.
* Plan rotation **before expiry**—an expired application instance certificate fails the handshake on both ends.

***

## Data Persistence & Recovery

By default a server route holds node values **in memory only**—a broker restart resets them (nodes return to `BadWaitingForInitialData` until the next publish). Set `WITH PERSISTENCE true` on `OPCUA_SERVER_CONFIG` to snapshot served values and restore them on startup.

```lot wrap focus={3-12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH PERSISTENCE true
    ADD MAPPING Nodes
        WITH SOURCE_TOPIC "plant/line1"
        ADD TAG Setpoint
            WITH DATA_TYPE "FLOAT"
            WITH WRITABLE true
        ADD TAG Temperature
            WITH DATA_TYPE "FLOAT"
```

The same **ownership-aware policy** as the [Modbus TCP Server](./modbus-tcp-server#data-persistence--recovery) route applies:

| Node | Owner | Default | On restart |
| - | - | - | - |
| `WRITABLE true` | Client-written setpoint lives only in the server | **Persisted** | Restored **authoritatively**—wins over retained MQTT; returns a good value instead of `BadWaitingForInitialData` |
| Read-only / MQTT-fed | Fed from live MQTT | **Not persisted** | Recovered from **live MQTT** (retained message / next publish) |

Client-written setpoints survive a restart; live telemetry always reflects the current MQTT source.

### Per-tag and mapping overrides

`WITH PERSISTENCE true|false` on an individual `ADD TAG`—or a whole `ADD MAPPING`—overrides the route default:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OverrideServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH PERSISTENCE true
    ADD MAPPING Control
        WITH SOURCE_TOPIC "plant/line1"
        ADD TAG Setpoint
            WITH DATA_TYPE "FLOAT"
            WITH WRITABLE true
        ADD TAG LastGoodTemp
            WITH DATA_TYPE "FLOAT"
            WITH PERSISTENCE "true"
    ADD MAPPING Scratch
        WITH SOURCE_TOPIC "plant/scratch"
        WITH PERSISTENCE "false"
        ADD TAG TestpointA
            WITH DATA_TYPE "FLOAT"
            WITH WRITABLE true
        ADD TAG TestpointB
            WITH DATA_TYPE "INT32"
            WITH WRITABLE true
```

* `Setpoint` — restored authoritatively to the last client-written value.
* `LastGoodTemp` — served from snapshot until `plant/line1/LastGoodTemp` next publishes.
* `Scratch` mapping tags — never snapshotted; start cold on every restart.

### Caching slow or infrequently-updated sources

MQTT-fed nodes are not persisted by default. When a source updates infrequently—an hourly energy total, a shift-latched value—force-persist with `WITH PERSISTENCE "true"` so the last received value is re-served on cold start until the next live publish overrides it.

```lot wrap focus={6-12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MeterServer WITH TYPE OPCUA_SERVER
    ADD OPCUA_SERVER_CONFIG
        WITH SERVER_PORT 4840
        WITH PERSISTENCE true
    ADD MAPPING Meters
        WITH SOURCE_TOPIC "plant/energy"
        ADD TAG TotalEnergy
            WITH DATA_TYPE "DOUBLE"
            WITH PERSISTENCE "true"
        ADD TAG DailyPeak
            WITH DATA_TYPE "DOUBLE"
            WITH PERSISTENCE "true"
```

After a restart, a client browsing `TotalEnergy` or `DailyPeak` reads the last published value—a good value rather than `BadWaitingForInitialData`—until the next publish refreshes it.

> [!WARNING]
>
> Persistence applies only to **explicitly-declared** `ADD TAG` nodes. Wildcard/dynamic nodes created from MQTT topic patterns are not snapshotted—a slow source behind a wildcard mapping still starts cold.

### Storage & durability

* Values are written through a non-blocking background worker on every change (rapid updates coalesce to the latest value).
* The snapshot lives in an encrypted-at-rest database under the broker data directory.
* A snapshot is keyed to the route's tag definitions by a fingerprint. If addresses or types change between runs, the stale snapshot is discarded rather than replayed into reshaped nodes.

***

## Verify Your Server

**1. Deploy the route**

Add the route definition to your broker and confirm route status shows **Connected** (the endpoint is listening).

**2. Connect with an OPC UA client**

Use UaExpert or any OPC UA browser. Connect to `opc.tcp://<broker-host>:<SERVER_PORT>` (append `ENDPOINT_PATH` if configured). When using security, trust the server certificate—export it from `PKI_DIR/own/certs` or follow the trust workflow in [Certificate Management](#certificate-management).

**3. Confirm MQTT → node**

Publish a test value to a bound topic—for example `factory/line1/Temperature`. Refresh the client and confirm the node value updates.

**4. Confirm node → MQTT**

For a writable tag, write a value from the OPC UA client. Subscribe to the tag's `SOURCE_TOPIC` with MQTT Explorer or any MQTT client and confirm the retained publish arrives.

***

## Limitations

> [!WARNING]
>
> Each `OPCUA_SERVER` route binds exclusively to its `SERVER_PORT`. A second route on the same port fails to start and reports a red route status.

***

## Troubleshooting

> **Route status Red — failed to start server**
>
> * **Port in use** — Another process or server route already uses `SERVER_PORT`. Choose a different port or stop the conflicting service.
> * **PKI directory not writable** — Ensure the broker can write to `PKI_DIR` (or the default PKI location).

> **Client rejects the server certificate on every reconnect**
>
> The application instance certificate regenerates when `PKI_DIR` is ephemeral—for example, an unmounted Docker volume. Mount `PKI_DIR` as a persistent volume so client trust stores remain valid across restarts.

> **Client certificate rejected**
>
> * **Client cert not trusted** — Move it from `PKI_DIR/rejected/certs` to `PKI_DIR/trusted/certs`, or set `AUTO_ACCEPT_UNTRUSTED_CERTIFICATES true` for testing only
> * **Client cert has no application URI SAN** — The connecting client must present a certificate with a valid URI in Subject Alternative Name

> **Strict client refuses the security handshake**
>
> `APPLICATION_URI` must match the certificate SAN URI. Leave both at their defaults, or change them together so they stay in sync.

> **Client write has no effect**
>
> * Confirm the tag has `WITH WRITABLE true`.
> * Subscribe to the tag's `SOURCE_TOPIC` and check for the retained publish after the write.
> * Verify `MIN_VALUE` / `MAX_VALUE` are not clamping the written value away from what you expect.

> **Client activity logging**
>
> The broker logs client activity for this route in the broker log:
>
> * **Sessions** — client session create and close events.
> * **Accepted external writes** — writes accepted on tags and on individual STRUCT fields.
>
> Use these log lines to confirm which clients connected and which node writes reached MQTT.

> **Anonymous connection rejected**
>
> `ALLOW_ANONYMOUS` is `false`. Connect with the configured username and password, or use certificate authentication if enabled.

> **Node reads BadWaitingForInitialData**
>
> The `SOURCE_TOPIC` has not published yet (or no retained value). Publish the topic, enable route persistence for client-written setpoints, or force-persist a slow MQTT-fed source with `WITH PERSISTENCE "true"`. See [Data persistence & recovery](#data-persistence--recovery).

***

## Best Practices

> **Persist PKI in production**
>
> Mount `PKI_DIR` as a persistent volume so server certificates survive broker restarts. Ephemeral storage regenerates the application instance certificate on every boot and breaks client trust stores. See [Certificate Management](#certificate-management).

> **Disable anonymous access**
>
> Set `ALLOW_ANONYMOUS false` on plant networks and require username/password or certificate authentication. Anonymous endpoints are appropriate for lab or isolated development only.

> **Reserve ports**
>
> Each server route binds exclusively to its `SERVER_PORT`. Document which route owns which port to avoid startup conflicts when adding routes.

> **Use secrets for credentials**
>
> Prefer `GET ENV` and `GET SECRET` over literals in route definitions. Never commit passwords or client credentials to version control.

> **Match security to the client**
>
> Align `SECURITY_POLICY` and `MESSAGE_SECURITY` with what your SCADA or HMI supports. If the client cannot negotiate encryption, enable `ALLOW_NO_SECURITY` for testing only—not in production.

> **Enable persistence for client-written setpoints**
>
> Set `WITH PERSISTENCE true` on `OPCUA_SERVER_CONFIG` so client-written values survive a broker restart instead of returning `BadWaitingForInitialData`. See [Data persistence & recovery](#data-persistence--recovery).

***

## Next Steps

> **[OPC UA Client Route](./opcua-client)**
>
> Connect to remote OPC UA servers and poll device data into MQTT.

> **[Industrial Routes Overview](./overview)**
>
> Compare protocols and choose the right industrial route for your architecture.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.