Configuration Reference

Configure the gateway from the web UI, seeded once from config.toml

Configuration File Format

As of v2.3.0, opcgw stores its configuration in SQLite and is managed from the web UI. The TOML file (config/config.toml) is a bootstrap seed: it is read once on first start to populate the database, then ignored at runtime. The sections below document the TOML schema used for that seed (and for OPCGW_* environment overrides); the same fields are editable in the web UI’s singleton-configuration editor after first boot, and applications/devices/metrics are managed through the ChirpStack inventory pickers.

First boot — the setup wizard

On a pristine install (placeholder secrets, no ChirpStack credentials), opcgw boots into first-run mode and serves a zero-touch setup wizard at http://<host>:8080/ (the /setup route). The wizard captures the ChirpStack server address, tenant ID and API token plus the OPC UA password, writes the secrets to config/secrets.toml and the rest to SQLite, and performs an in-process soft restart. No text-file editing is required — see the Quick Start Guide for the step-by-step flow.

Staged “Apply changes”

After first boot, edits made in the web UI stage into SQLite instead of taking effect immediately. While changes are pending, GET /api/status reports pending_changes: true; you commit them all at once with a single Apply changes action (POST /api/config/apply), which soft-restarts the data plane in-process. The container is never restarted.

Backup / portability

The current effective configuration can be exported and re-imported as TOML:

  • GET /api/config/export — download the configuration as TOML. Secrets are excluded from the export.
  • POST /api/config/import — submit a TOML config (as produced by export). The import is staged, not applied inline; commit it with Apply changes (POST /api/config/apply).

Secrets

api_token and user_password ship as REPLACE_ME_WITH_* placeholders that the gateway recognises (it boots into the setup wizard rather than failing). Provide the real values through any of these paths:

  • Setup wizard (primary, first boot) — writes the secrets to config/secrets.toml with 0600 permissions.
  • config/secrets.toml — the persisted secrets file (chmod 0600); the wizard manages it, but you may pre-create it for unattended deployments.
  • Environment variablesOPCGW_CHIRPSTACK__API_TOKEN, OPCGW_OPCUA__USER_PASSWORD (highest precedence; these two secrets are on the v2.8.0 env allowlist — most other OPCGW_* overrides are ignored, see “Environment Variable Overrides” below).

See docs/security.md for the env var convention, the Docker / Kubernetes recipe, and the migration path for existing deployments. Never store plaintext secrets inline in config.toml.

Global Structure

[global]
# Global settings

[chirpstack]
# ChirpStack connection parameters

[opcua]
# OPC UA server parameters

[[application]]
# One or more applications

[global] Section

Global application settings.

Parameters

Parameter Type Default Description
debug bool false Enable debug logging (more verbose)
prune_interval_minutes u32 60 How often (minutes) opcgw prunes expired stored metric/command history.
history_retention_days u32 7 How many days of metric/command history to keep before pruning.
command_delivery_poll_interval_secs u64 5 How often opcgw polls ChirpStack for command delivery confirmations (must be >= 1).
command_delivery_timeout_secs u32 60 A command left in the “sent” state longer than this is marked failed (must be >= 1).
command_dispatch_deadline_secs u32 120 Story J-1 / #182. A command still Pending this long after creation is marked Failed (command_dispatch_expired) and never delivered, so a stale command cannot actuate hardware late; it also bounds the dispatcher’s retry ladder while ChirpStack is unreachable. Independent of command_delivery_timeout_secs (which is the confirmation sweep): on a slow-cadence LoRaWAN deployment the confirmation timeout may need to be minutes, and coupling the two would deliver very old commands. Raise this if your ChirpStack restarts take longer than the default. Must be >= 1.
command_timeout_check_interval_secs u64 10 How often (seconds) opcgw sweeps for timed-out commands.

Example

[global]
debug = true  # Set to false in production for better performance
command_delivery_poll_interval_secs = 5
command_delivery_timeout_secs = 60
# Dispatch deadline (#182): a command still Pending this long after creation is
# failed rather than delivered late. Independent of the confirmation timeout
# above — raise it if your ChirpStack restarts take longer than this.
command_dispatch_deadline_secs = 120

[chirpstack] Section

Configuration for ChirpStack connection and polling behavior.

Parameters

Parameter Type Required Description
server_address string ChirpStack gRPC server address (format: http://host:port)
api_token string API authentication token (from ChirpStack UI → Settings → API Keys)
tenant_id string Tenant ID (from ChirpStack UI → Tenants)
polling_frequency u64 Seconds between polls (must be > 0)
retry u32 Maximum retry attempts on connection failure (must be > 0)
delay u64 Milliseconds to wait between retry attempts (must be > 0)
stream_all_devices bool Default false. When true, opcgw subscribes to the gRPC uplink event stream for all devices (not just command-class devices). The streamed device set is fixed at startup (restart-required). Managed on the web Admin page (since v2.8.0 the former env override OPCGW_CHIRPSTACK__STREAM_ALL_DEVICES is ignored — see “Environment Variable Overrides”).
list_page_size u32 Page size for ChirpStack list calls (applications/devices). Tuning knob; default 100.
inventory_cache_ttl_seconds u64 How long (seconds) the web UI caches ChirpStack inventory (applications/devices/measurements) before refetching. Default 60.
inventory_uplink_max_wait_seconds u64 Maximum seconds the metric picker waits when reading recent uplinks for a device. Default 5.

Validation Rules

  • server_address: Must be valid URL with http:// or https://
  • api_token: Non-empty string
  • tenant_id: Non-empty string
  • polling_frequency: > 0 (recommended: 5-300 seconds)
  • retry: > 0 (recommended: 3-10)
  • delay: > 0 (recommended: 100-1000 ms)

Example

[chirpstack]
server_address = "http://chirpstack.example.com:8080"
api_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."  # Get from ChirpStack UI
tenant_id = "tes-tenant-id"
polling_frequency = 10      # Poll every 10 seconds
retry = 3                   # Retry 3 times on failure
delay = 100                 # Wait 100ms between retries

Obtaining Credentials

API Token:

  1. Login to ChirpStack UI
  2. Navigate to Settings → API Keys
  3. Create new API key with “api” permission
  4. Copy token value

Tenant ID:

  1. In ChirpStack UI, navigate to Tenants
  2. Click on your tenant
  3. Copy the tenant ID from URL or details page

[opcua] Section

Configuration for OPC UA server.

Parameters

Parameter Type Required Default Description
application_name string - Display name for OPC UA server
application_uri string - Unique URI (e.g., urn:company:opcua:gateway)
product_uri string - Product identifier
host_ip_address string 0.0.0.0 IP address to bind to
host_port u16 4840 Port to listen on (0-65535, must not be 0)
certificate_path string own/cert.der Path to certificate file
private_key_path string private/private.pem Path to private key file
pki_dir string ./pki Directory for PKI files
create_sample_keypair bool false Auto-generate self-signed cert if missing
user_name string - OPC UA client username
user_password string - OPC UA client password
stale_threshold_seconds u64 2× polling freq Age (seconds) past which a metric’s OPC UA status code degrades to Uncertain; metrics older than 24 h return Bad. Range (0, 86400]. Can be overridden per device (see [[application.device]]).
diagnostics_enabled bool false Enable OPC UA diagnostics
trust_client_cert bool false Accept any client certificate
check_cert_time bool false Validate certificate expiration
hello_timeout u64 5 Seconds to wait for hello message
max_connections usize - Maximum concurrent OPC UA client sessions (optional cap).
max_subscriptions_per_session usize - Maximum OPC UA subscriptions allowed per client session (optional cap).
max_monitored_items_per_sub usize - Maximum monitored items per OPC UA subscription (optional cap).
max_message_size usize - Maximum OPC UA message size in bytes (optional cap).
max_chunk_count usize - Maximum number of chunks per OPC UA message (optional cap).
max_history_data_results_per_node usize - Maximum history values returned per node in one OPC UA history read (optional cap).
max_keep_alive_count u32 0 Issue #155: caps the granted OPC UA subscription KeepAliveCount. An idle subscription sends a keep-alive every KeepAliveCount × publishingInterval; lowering this cap forces keep-alives more frequently, which SCADA clients (e.g. Ignition) may need to avoid marking slow-changing tags Uncertain_LastKnownValue. Range [0, 1000], 0 = library default. Editable from the web Admin page.
min_publishing_interval_ms f64 0 Issue #155: minimum OPC UA subscription publishing interval (ms) the server will grant — a floor; the client (e.g. Ignition Tag Group Rate) still requests the actual rate at or above it. Range [0, 3600000], 0 = library default. Editable from the web Admin page.

Validation Rules

  • application_name: Non-empty string
  • application_uri: Non-empty string (typically URN format)
  • user_name: Non-empty string
  • user_password: Non-empty string
  • host_port: > 0 if specified

Security Recommendations

[opcua]
application_name = "My IoT Gateway"
application_uri = "urn:my-company:opcua:gateway"

# Network binding
host_ip_address = "10.0.1.50"   # Bind to specific IP on private network
host_port = 4840

# Authentication
user_name = "operator"
user_password = "strong_password_123"  # or use env var: OPCGW_OPCUA__USER_PASSWORD

# Security (production)
create_sample_keypair = false   # Use proper certs, not sample
trust_client_cert = false       # Verify client certificates
check_cert_time = true          # Reject expired certs
diagnostics_enabled = false     # Reduce attack surface

# PKI
pki_dir = "/etc/opcgw/pki"      # Restricted permissions
certificate_path = "/etc/opcgw/pki/cert.der"
private_key_path = "/etc/opcgw/pki/key.pem"

Example (Development)

[opcua]
application_name = "My Test Gateway"
application_uri = "urn:my-test:gateway"
host_ip_address = "0.0.0.0"
host_port = 4840
user_name = "admin"
user_password = "password"
pki_dir = "./pki"
create_sample_keypair = true   # OK for testing

[web] Section

Configuration for the embedded web UI (setup wizard, status dashboard, live metrics, ChirpStack inventory pickers, drift view, and singleton-configuration editor). The web UI is the primary way to configure opcgw and is enabled by default in the shipped config.toml (the binary’s built-in default is enabled = false).

Parameters

Parameter Type Required Default Description
enabled bool false (binary) / true (shipped config) Master switch for the web server
port u16 8080 Listening port (range 1024–65535)
bind_address string 0.0.0.0 IP address to bind to
auth_realm string opcgw HTTP Basic auth realm (max 64 chars)
allowed_origins array ["http://<bind_address>:<port>"] CSRF allow-list for state-changing requests (each entry scheme://host[:port])

Authentication reuses the [opcua].user_name / [opcua].user_password credentials — there is no separate web admin account. The server is HTTP-only; put a reverse proxy in front of it for TLS.

Example

[web]
enabled = true
port = 8080
bind_address = "0.0.0.0"
auth_realm = "opcgw"
allowed_origins = ["http://127.0.0.1:8080", "http://localhost:8080"]

[[application]] Section

Define ChirpStack applications to expose in OPC UA. These are a bootstrap seed only — after first boot, manage applications/devices/metrics through the web UI’s ChirpStack inventory pickers.

Top-Level Parameters

Parameter Type Required Description
application_name string Display name in OPC UA address space
application_id string ChirpStack application ID (as string)

Validation Rules

  • Each application must have unique application_id
  • application_name: Non-empty
  • application_id: Non-empty
  • At least one device per application

Example

[[application]]
application_name = "Farm Network"
application_id = "1"
# ... devices follow below

[[application]]
application_name = "Building Automation"
application_id = "2"
# ... devices follow below

[[application.device]] Section

Define devices under an application.

Parameters

Parameter Type Required Description
device_name string Display name in OPC UA
device_id string ChirpStack device ID
stale_threshold_seconds u64 Per-device override of [opcua].stale_threshold_seconds. Range (0, 86400]. Set above the device’s report period so a slow-but-healthy LoRaWAN sensor reads Good between uplinks. None = use the global. Restart-required.
source_timestamp_server bool Issue #153: per-device OPC UA SourceTimestamp mode. false (default) stamps served values with the device’s real report time (strict OPC UA semantics). true stamps the gateway’s current time (now()) so SCADA clients (e.g. Ignition) that flag old source timestamps keep the tag Good — common on slow-cadence LoRaWAN devices. Does not change the staleness StatusCode (a genuinely dead device still reads Uncertain). Settable from the web UI checkbox. Restart-required (Apply).

Validation Rules

  • Each device_id must be unique across ALL applications
  • device_name: Non-empty
  • device_id: Non-empty
  • At least one metric per device

Example

[[application]]
application_name = "Farm Network"
application_id = "1"

[[application.device]]
device_name = "Field A Sensor"
device_id = "sensor_001"
# ... metrics follow below

[[application.device]]
device_name = "Field B Sensor"
device_id = "sensor_002"
# ... metrics follow below

[[application.device.read_metric]] Section

Define metrics (data points) from a device.

Parameters

Parameter Type Required Description
metric_name string Display name in OPC UA (the variable name)
chirpstack_metric_name string Field name from ChirpStack device data
metric_type string Data type: Float, Int, Bool, String
metric_unit string Unit of measurement (e.g., “°C”, “%”, “kW”)

Validation Rules

  • metric_name: Non-empty
  • chirpstack_metric_name: Non-empty (must match ChirpStack field exactly)
  • metric_type: One of Float, Int, Bool, String
  • metric_unit: Optional, any string

Metric Types

Type Example OPC UA Type Notes
Float 23.5, 85.0 Double Temperature, humidity, percentages
Int 42, 1000 Int32 Counts, thresholds, status codes
Bool true, false Boolean On/off, running/stopped, present/absent
String “OK”, “ERROR” String Status messages, device names

Example

[[application.device.read_metric]]
metric_name = "Soil Moisture"
chirpstack_metric_name = "soil_moisture_pct"
metric_type = "Float"
metric_unit = "%"

[[application.device.read_metric]]
metric_name = "Device Status"
chirpstack_metric_name = "status"
metric_type = "String"

[[application.device.read_metric]]
metric_name = "Alert Flag"
chirpstack_metric_name = "alert_active"
metric_type = "Bool"

[[application.device.read_metric]]
metric_name = "Message Count"
chirpstack_metric_name = "message_count"
metric_type = "Int"

[[application.device.command]] Section

Define downlink commands that OPC UA clients can issue to a device (Epic E, v2.2.0). Each command becomes a writable OPC UA node; writing the canonical value enqueues a ChirpStack downlink, and opcgw tracks delivery confirmation (see [global].command_delivery_*).

Parameters

Parameter Type Required Description
command_id i32 Unique command identifier
command_name string Display name of the command node in OPC UA
command_confirmed bool Whether ChirpStack must return a delivery confirmation for this command
command_port i32 LoRaWAN FPort the downlink is sent on
command_class string Optional device-class binding. When absent, the OPC UA write is delivered as raw payload bytes on command_port (legacy, model-specific path). When set, the canonical OPC UA value is translated into a semantic command object handled by the ChirpStack device-profile codec, keeping opcgw model-agnostic. Currently "valve" is recognised (canonical 1 → open, 0 → close). An unknown class fails validation.

Example

[[application.device.command]]
command_name = "Open_close_valve01"
command_id = 1
command_confirmed = true
command_port = 10
# command_class = "valve" maps canonical OPC UA 1 (open) / 0 (close) to the
# semantic {"command": "open"/"close"} the device-profile codec encodes.
command_class = "valve"

Complete Configuration Example

[global]
debug = true

[chirpstack]
server_address = "http://chirpstack.local:8080"
api_token = "your-api-token-here"
tenant_id = "your-tenant-id"
polling_frequency = 10
retry = 3
delay = 100

[opcua]
application_name = "IoT Gateway"
application_uri = "urn:mycompany:opcua:gateway"
host_ip_address = "0.0.0.0"
host_port = 4840
user_name = "admin"
user_password = "changeme"
pki_dir = "./pki"
create_sample_keypair = true

# Application 1: Farm Sensors
[[application]]
application_name = "Farm Network"
application_id = "1"

[[application.device]]
device_name = "Field A - North"
device_id = "farm_001"

[[application.device.read_metric]]
metric_name = "Soil Moisture"
chirpstack_metric_name = "moisture"
metric_type = "Float"
metric_unit = "%"

[[application.device.read_metric]]
metric_name = "Temperature"
chirpstack_metric_name = "temp"
metric_type = "Float"
metric_unit = "°C"

[[application.device]]
device_name = "Field B - South"
device_id = "farm_002"

[[application.device.read_metric]]
metric_name = "Soil Moisture"
chirpstack_metric_name = "moisture"
metric_type = "Float"
metric_unit = "%"

# Application 2: Building Management
[[application]]
application_name = "Building Automation"
application_id = "2"

[[application.device]]
device_name = "Floor 1 HVAC"
device_id = "hvac_f1"

[[application.device.read_metric]]
metric_name = "Temperature Setpoint"
chirpstack_metric_name = "temp_setpoint"
metric_type = "Float"
metric_unit = "°C"

[[application.device.read_metric]]
metric_name = "Fan Status"
chirpstack_metric_name = "fan_on"
metric_type = "Bool"

Environment Variable Overrides

Since v2.8.0 (Story J-2 / #169), environment overrides are restricted to an allowlist. The web Admin page (backed by SQLite) is the authoritative source for the editable configuration; an OPCGW_<SECTION>__<FIELD> env var that addresses a web-editable field outside the allowlist is ignored and reported once per boot with a env_var_ignored WARN naming the variable. (Before v2.8.0 any env var silently outranked the Admin page — the env-shadows-database trap, #168.)

Format for the vars that ARE env-capable: OPCGW_<SECTION>__<FIELD> — note the double underscore (__) between the section and the field name.

The allowlist

Env var Why it stays env-capable
OPCGW_CHIRPSTACK__API_TOKEN Secret — never stored in SQLite; needed before SQLite exists (first boot / unattended provisioning)
OPCGW_OPCUA__USER_PASSWORD Secret — same rationale (also the web Basic-auth password)
OPCGW_WEB__ENABLED, OPCGW_WEB__BIND_ADDRESS, OPCGW_WEB__PORT Bootstrap: the gateway must be able to serve /setup before SQLite exists; OPCGW_WEB__PORT also drives docker-compose port publishing
OPCGW_OPCUA__HOST_PORT docker-compose port mapping + container healthcheck

Sections that are not web-editable are unaffected by the filter and remain fully env-overridable: [storage] (e.g. OPCGW_STORAGE__DATABASE_PATH), [command_validation], and [logging] (OPCGW_LOGGING__DIR, OPCGW_LOGGING__LEVEL). The short-form/process-level knobs below are likewise untouched.

Everything else in [global], [chirpstack], [opcua], [web] — including OPCGW_CHIRPSTACK__SERVER_ADDRESS, OPCGW_CHIRPSTACK__POLLING_FREQUENCY, OPCGW_CHIRPSTACK__STREAM_ALL_DEVICES, OPCGW_OPCUA__USER_NAME, OPCGW_GLOBAL__DEBUG, OPCGW_OPCUA__STALE_THRESHOLD_SECONDS — is managed on the web Admin page. If you still set such a var, it is ignored (WARN) and the Admin-page value wins.

Examples

# Secrets for unattended provisioning (allowlisted)
export OPCGW_CHIRPSTACK__API_TOKEN="eyJ0eXAi..."
export OPCGW_OPCUA__USER_PASSWORD="a-strong-password"

# Bootstrap ports (allowlisted)
export OPCGW_OPCUA__HOST_PORT="4860"
export OPCGW_WEB__ENABLED="true"

# IGNORED since v2.8.0 (web/SQLite-managed — set it on the Admin page):
#   export OPCGW_CHIRPSTACK__SERVER_ADDRESS="http://prod-chirpstack:8080"
#   export OPCGW_CHIRPSTACK__POLLING_FREQUENCY="30"

# Run with overrides
cargo run --release

Precedence

  1. Environment variables — allowlisted vars only (highest priority)
  2. SQLite — the live configuration store (authoritative for everything the web Admin page edits; non-allowlisted env vars cannot outrank it)
  3. config.toml — the bootstrap seed (first boot only)
  4. Built-in defaults (lowest priority)

Process-level / short-form knobs (env-only, always applied)

These are read directly from the environment at startup — they never pass through the figment layer, so the v2.8.0 allowlist does not affect them:

Env var Default Description
CONFIG_PATH config/config.toml Path to the seed config file (unprefixed; the -c CLI flag wins over it).
OPCGW_LOG_DIR ./log Log directory. Short form (no __); wins over [logging].dir.
OPCGW_LOG_LEVEL info Log verbosity (error/warn/info/debug/trace); the -d CLI flag wins over it.
OPCGW_ERROR_EVENT_CAP 500 Story G-4 (#127): maximum number of recent error events retained in the bounded feed behind the dashboard error drill-down (GET /api/errors/errors.html). Oldest events are pruned beyond this cap. A positive integer; invalid/zero values fall back to the default.
OPCGW_STORAGE_QUERY_BUDGET_MS 250 Latency above which a storage-query timing log is upgraded from debug to warn.
OPCGW_BATCH_WRITE_BUDGET_MS 2000 Latency above which a batch-write timing log is emitted at warn.

Troubleshooting Configuration

Validation Error: “URL format”

Configuration validation failed:
  - chirpstack.server_address: invalid URL format

Fix: Ensure server_address includes protocol:

# Wrong
server_address = "localhost:8080"

# Correct
server_address = "http://localhost:8080"

No applications configured

An empty application list is valid on a fresh install — opcgw boots into the setup wizard, and you add applications/devices/metrics from the web UI’s ChirpStack inventory pickers. It is not a hard startup failure. Pre-seed [[application]] blocks in config.toml only if you prefer an unattended seed.

Metrics not appearing in OPC UA

Check:

  1. Are metric names spelled correctly in config vs. ChirpStack?
  2. Are they in correct metric type (Float, Int, Bool, String)?
  3. Check ChirpStack device details for exact field names
  4. Run with debug = true and check logs

“Port already in use”

error: Failed to bind to 0.0.0.0:4840

Fix: Change host_port or kill process using it:

lsof -i :4840
kill <PID>

Best Practices

  1. Credentials: Never hardcode in config files. Use environment variables for production.
    export OPCGW_CHIRPSTACK__API_TOKEN="your-token"
    export OPCGW_OPCUA__USER_PASSWORD="your-password"
    
  2. Polling Interval: Start with 10-30 seconds, adjust based on:
    • How fresh do you need data? (lower = fresher but more load)
    • How many devices? (more devices = less frequent polling)
    • ChirpStack load capacity (ask your admin)
  3. Naming: Use consistent, clear names
    # Good: descriptive, matches actual location/purpose
    device_name = "Greenhouse A - Temperature Probe"
    metric_name = "Internal Air Temperature"
    
    # Bad: vague or generic
    device_name = "Sensor 1"
    metric_name = "Data"
    
  4. Organization: Group related applications
    [[application]]
    application_name = "Production - Line 1"
    application_id = "prod_line_1"
       
    [[application]]
    application_name = "Production - Line 2"
    application_id = "prod_line_2"
    
  5. Testing: Validate config before deployment
    cargo run --release 2>&1 | head -20  # Check startup messages
    

For more details, see the Quick Start Guide and Architecture sections.