> ## 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

<Info>
  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).
</Info>

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.
</Tip>

## 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

<Tabs>
  <Tab title="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`.
  </Tab>

  <Tab title="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).
  </Tab>

  <Tab title="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.
  </Tab>

  <Tab title="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.
  </Tab>

  <Tab title="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.
  </Tab>
</Tabs>

***

## 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

<ParamField path="SERVER_PORT" type="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.
</ParamField>

<ParamField path="ENDPOINT_PATH" type="string">
  Optional path appended to the base address, for example `/coreflux`. Default: empty.
</ParamField>

<ParamField path="APPLICATION_NAME" type="string" default="CorefluxBroker_OpcUaServer">
  Application display name and certificate common name (CN).
</ParamField>

<ParamField path="APPLICATION_URI" type="string" default="urn:{host}:Coreflux:{route}">
  Application URI. Must match the certificate SAN URI. Changing this regenerates the certificate.
</ParamField>

<ParamField path="NAMESPACE_URI" type="string" default="urn:coreflux:opcua:{route}">
  Namespace URI for exposed nodes.
</ParamField>

<ParamField path="ROOT_FOLDER" type="string" default="route name">
  Root folder under `Objects` where tags are organized.
</ParamField>

<ParamField path="PKI_DIR" type="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).
</ParamField>

### Security

<ParamField path="USE_SECURITY" type="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`.
  </Note>
</ParamField>

<ParamField path="SECURITY_POLICY" type="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  |
</ParamField>

<ParamField path="MESSAGE_SECURITY" type="integer" default="0">
  Message security mode. Requires `SECURITY_POLICY` ≥ 1 when using Sign or SignAndEncrypt.

  | Value | Mode           |
  | ----- | -------------- |
  | `0`   | None           |
  | `1`   | Sign           |
  | `2`   | SignAndEncrypt |
</ParamField>

<ParamField path="ALLOW_NO_SECURITY" type="boolean" default="false">
  When `USE_SECURITY` is true, also offer a None/None endpoint. Use for testing only.
</ParamField>

### Authentication

<ParamField path="ALLOW_ANONYMOUS" type="boolean" default="true">
  Offer anonymous login. Set to `false` for production and configure `USERNAME`/`PASSWORD` or `ALLOW_CERTIFICATE_AUTH`.
</ParamField>

<ParamField path="USERNAME" type="string">
  Username for password-based login. Use `GET ENV` or `GET SECRET`.
</ParamField>

<ParamField path="PASSWORD" type="string">
  Password for password-based login. Use `GET ENV` or `GET SECRET`.
</ParamField>

<ParamField path="ALLOW_CERTIFICATE_AUTH" type="boolean" default="false">
  Offer X.509 certificate user authentication.
</ParamField>

<ParamField path="AUTO_ACCEPT_UNTRUSTED_CERTIFICATES" type="boolean" default="false">
  Automatically trust connecting client certificates. When false, untrusted client certs are placed in the `rejected` store.
</ParamField>

<ParamField path="PERSISTENCE" type="boolean" default="false">
  Persist served node values across broker restarts. See [Data persistence & recovery](#data-persistence--recovery).
</ParamField>

<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.
</Note>

***

## 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:

<ParamField path="WRITABLE" type="boolean" default="false">
  Allow OPC UA clients to write this node. Writes publish to the tag's `SOURCE_TOPIC`.
</ParamField>

<ParamField path="DECIMAL_PLACES" type="integer" default="2">
  Decimal places applied to `FLOAT`/`DOUBLE` values when a client write is published to MQTT.
</ParamField>

<ParamField path="MIN_VALUE" type="double">
  Minimum value after a client write. Values below this are clamped.
</ParamField>

<ParamField path="MAX_VALUE" type="double">
  Maximum value after a client write. Values above this are clamped.
</ParamField>

<ParamField path="PERSISTENCE" type="boolean">
  Per-tag override of the route persistence default. Precedence: tag → mapping → route. See [Data persistence & recovery](#data-persistence--recovery).
</ParamField>

### 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).

***

## 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.
</Note>

### 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.
</Warning>

***

## 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.

<ParamField path="DATA_TYPE" type="string">
  Set to `"ARRAY"` for list values.
</ParamField>

<ParamField path="ARRAY_ELEMENT_TYPE" type="string" default="INT16">
  Type of each element: `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, or `STRING`.
</ParamField>

<ParamField path="ARRAY_SIZE" type="integer">
  Number of elements in the array. Set this to match your payload length. If omitted, the dimension is unspecified.
</ParamField>

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`.

<ParamField path="DATA_TYPE" type="string">
  Set to `"STRUCT"` for object payloads.
</ParamField>

<ParamField path="STRUCT_FIELD_DEFINITIONS" type="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.
</ParamField>

```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.
</Note>

***

## Security Examples

<Tabs>
  <Tab title="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"
    ```
  </Tab>

  <Tab title="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
    ```
  </Tab>

  <Tab title="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"
    ```
  </Tab>
</Tabs>

***

## 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.**
</Warning>

### 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:

<Steps>
  <Step title="First connect">
    The client's certificate lands in `PKI_DIR/rejected/certs`.
  </Step>

  <Step title="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/
    ```
  </Step>

  <Step title="Reconnect">
    The client connects successfully.
  </Step>
</Steps>

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.
</Warning>

### 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

<Steps>
  <Step title="Deploy the route">
    Add the route definition to your broker and confirm route status shows **Connected** (the endpoint is listening).
  </Step>

  <Step title="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).
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

***

## 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.
</Warning>

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="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).
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="Anonymous connection rejected">
    `ALLOW_ANONYMOUS` is `false`. Connect with the configured username and password, or use certificate authentication if enabled.
  </Accordion>

  <Accordion title="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).
  </Accordion>
</AccordionGroup>

***

## Best Practices

<AccordionGroup>
  <Accordion title="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).
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="Reserve ports">
    Each server route binds exclusively to its `SERVER_PORT`. Document which route owns which port to avoid startup conflicts when adding routes.
  </Accordion>

  <Accordion title="Use secrets for credentials">
    Prefer `GET ENV` and `GET SECRET` over literals in route definitions. Never commit passwords or client credentials to version control.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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).
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="OPC UA Client Route" icon="sitemap" href="./opcua-client">
    Connect to remote OPC UA servers and poll device data into MQTT.
  </Card>

  <Card title="Industrial Routes Overview" icon="industry" href="./overview">
    Compare protocols and choose the right industrial route for your architecture.
  </Card>
</CardGroup>
