---
title: Verdalia Relay Setup
tag: verdalia-relay-setup
summary: >-
  Install, configure, and run Verdalia Relay on your local machine. Covers static
  and dynamic modes, config file, CLI reference, TLS, filesystem mounts,
  coding agents (OpenCode or Oh My Pi), and service setup.
---

# Verdalia Relay

Verdalia Relay lets workflows and assistants use services running on your own machine. Use it for local filesystem access, coding-agent sessions with OpenCode or Oh My Pi, Hue lights, or other Relay-backed tools without giving Canopy direct access to your computer.

## Installation

Relay is a single executable. Download it from the [Downloads page](/downloads), verify it, and unpack it.

### Linux

```bash
sha256sum -c SHA256SUMS
tar -xzf verdalia-relay-linux-x86_64.tar.gz
./verdalia-relay --help
```

Needs glibc 2.36 or newer and ALSA for audio playback. Both are present on any desktop Linux with working sound.

### Windows

Unpack the zip anywhere — there is no installer, and nothing is written outside the folder you unpack into and your user profile. Then, from PowerShell in that folder:

```powershell
Get-FileHash verdalia-relay-windows-x86_64.zip -Algorithm SHA256
.\verdalia-relay.exe --help
```

Compare the printed hash against the one in `SHA256SUMS`. Windows 10 or newer, 64-bit. Nothing else to install: audio playback goes through WASAPI, which is part of Windows.

Two things behave differently on Windows, and both are noted where they matter below: configuration lives under `%APPDATA%\verdalia-relay` rather than `~/.config`, and filesystem mount paths are Windows paths.

The executable is unsigned, so SmartScreen shows a warning the first time you run it. Verify the checksum above, then choose **More info → Run anyway**.

## Configuration

Relay keeps its settings in a single `config.json`, created with defaults on first run. There are no environment variables except `RUST_LOG` for log level.

| Platform | Location |
|----------|----------|
| Linux | `~/.config/verdalia-relay/config.json` |
| Windows | `%APPDATA%\verdalia-relay\config.json` |

Settings are read once, when `serve` starts. A change takes effect the next time Relay starts.

### Plugins

Each capability Relay offers is a plugin with its own switch, and every switch is on by default. A plugin that is on still needs what it works with:

| Plugin | Name in commands | Needs |
|--------|------------------|-------|
| Hue | `hue` | A paired bridge — see [Hue Bridge](#hue-bridge) |
| Filesystem | `filesystem` | At least one [mount](#filesystem-mounts) |
| Coding agent | `opencode_acp` | At least one mount, and [OpenCode or Oh My Pi](#coding-agent) installed |
| Audio | `audio` | A sound output device |

Filesystem and the coding agent wait while no mount exists; `verdalia-relay status` lists them as `on, waiting for a filesystem mount`. A request to a plugin that is off or waiting is answered with a 503 that names what to do.

### Changing settings

Run `verdalia-relay config` in a terminal to open the settings editor: sections on the left, their settings on the right. Arrow keys move, **Space** flips a switch, **Enter** edits a value, **r** resets it to its default, and **s** saves. In the **Mounts** section, **a** adds a mount and **d** removes one. The editor will not save a combination Relay would refuse to start with, and names the problem at the bottom of the screen.

The same settings are available as commands, for scripts and remote shells:

```bash
verdalia-relay config show [--format json]     # every setting
verdalia-relay config get server.port
verdalia-relay config set server.port 9000     # lists are comma-separated
verdalia-relay config unset server.port        # back to the default
verdalia-relay config disable audio            # or: enable <plugin>
verdalia-relay config mount add projects ~/projects [--read-only]
verdalia-relay config mount remove projects
verdalia-relay config mount list
verdalia-relay config path                     # where config.json is
```

`verdalia-relay config set --help` lists every setting with what it does. A value that is out of range is refused and the file is left as it was. The `dynamic` section is written by `verdalia-relay dynamic pair` and is not settable here.

### The config file

Editing the file by hand works too. A config with one mount looks like this:

```json
{
  "version": 2,
  "server": {
    "host": "127.0.0.1",
    "port": 8080,
    "allowed_origins": [
      "http://localhost:8081",
      "https://api.verdalia.io"
    ],
    "tls_cert_path": null,
    "tls_key_path": null
  },
  "hue": { "enabled": true },
  "filesystem": {
    "enabled": true,
    "mounts": [
      {
        "name": "workspace",
        "path": "/absolute/path/to/mount",
        "read_only": false
      }
    ]
  },
  "opencode_acp": {
    "enabled": true,
    "agent": "opencode",
    "opencode": { "command": "opencode", "args": ["acp"] },
    "omp": { "command": "omp", "args": ["acp"] },
    "max_sessions": 32
  },
  "dynamic": {
    "enabled": false,
    "api_base_url": null,
    "websocket_url": null,
    "relay_identity_id": null,
    "connection_id": null,
    "workspace_id": null,
    "machine_id": null,
    "private_key_path": null
  },
  "audio": {
    "enabled": true,
    "max_clip_bytes": 16777216,
    "max_concurrent_clips": 8
  }
}
```

**Note**: a config file without `"version"` is from an earlier Relay. The first command that reads it turns every plugin on and adds `"version": 1`; switch a plugin off again with `verdalia-relay config disable <plugin>`.

### Server Configuration

| Field | Default | Description |
|-------|---------|-------------|
| `host` | `127.0.0.1` | Bind address for the HTTP server. Use `0.0.0.0` for all interfaces (required when reverse-proxying). |
| `port` | `8080` | HTTP port. |
| `allowed_origins` | Canopy origins | CORS allowed origins. Empty array allows all origins (insecure). |
| `tls_cert_path` | `null` | Path to TLS certificate file. Set alongside `tls_key_path` to enable HTTPS. |
| `tls_key_path` | `null` | Path to TLS private key file. |

CLI flags `--host` and `--port` on the `serve` subcommand override config file values. Config file values override built-in defaults.

**TLS:** When both `tls_cert_path` and `tls_key_path` are set, the main server runs HTTPS. Otherwise HTTP. For production, consider reverse-proxying through Nginx or Caddy instead.

### Filesystem Mounts

Filesystem mounts define which local directories Relay exposes to authenticated requests. Add them with the editor's **Mounts** section or from the command line:

```bash
verdalia-relay config mount add workspace /home/user/projects
verdalia-relay config mount add docs /home/user/documents --read-only
```

A relative path is resolved against the current directory before it is stored. In the file, the same two mounts are:

```json
{
  "filesystem": {
    "enabled": true,
    "mounts": [
      { "name": "workspace", "path": "/home/user/projects", "read_only": false },
      { "name": "docs", "path": "/home/user/documents", "read_only": true }
    ]
  }
}
```

Each mount requires:

- **name** — lower case, digits, hyphens, underscores only (e.g. `workspace`, `my-project`)
- **path** — absolute path to an existing directory. On Windows that means a drive letter, and because the config file is JSON, each backslash is doubled: `"C:\\Users\\you\\projects"`.
- **read_only** — when `true`, write operations (write, append, edit, create, delete, move) are rejected at the API level

Mount names appear in workflow nodes and assistant tools as selectable targets. With no mounts, the Filesystem and coding-agent plugins wait and answer 503.

### Coding agent

Relay can host coding-agent sessions over the Agent Client Protocol, each running inside one of the mounts, with either [OpenCode](https://opencode.ai) or Oh My Pi (`omp`). A Relay runs one of them: `agent` picks which, and Agent Coding and the Relay ACP node list, start, and open that agent's sessions. It is on by default and starts working once a mount exists; turn it off with `verdalia-relay config disable opencode_acp`.

```bash
verdalia-relay config set opencode_acp.agent omp    # or: opencode
```

```json
{
  "opencode_acp": {
    "enabled": true,
    "agent": "opencode",
    "opencode": { "command": "opencode", "args": ["acp"] },
    "omp": { "command": "omp", "args": ["acp"] },
    "max_sessions": 32
  }
}
```

| Field | Default | Description |
|-------|---------|-------------|
| `enabled` | `true` | Run coding-agent sessions. Waits until at least one filesystem mount exists. |
| `agent` | `opencode` | The agent every session runs: `opencode` or `omp`. |
| `opencode.command` | `opencode` | OpenCode binary path or name. |
| `opencode.args` | `["acp"]` | Arguments that start OpenCode in ACP mode. |
| `omp.command` | `omp` | Oh My Pi binary path or name. |
| `omp.args` | `["acp"]` | Arguments that start Oh My Pi in ACP mode. |
| `max_sessions` | `32` | Most agents running at once; a session keeps its agent until it is closed. Minimum: 1. |

The agent runs as your user with its own configuration and credentials: log in to OpenCode or Oh My Pi on this machine first. By default Oh My Pi asks before running a shell command; its approval mode decides (`tools.approvalMode` in its config, or add `--approval-mode` to `omp.args`). Relay names each new Oh My Pi session after your first message, and `/rename <title>` changes the name.

**On Windows**, set `command` to the full path of the executable, or to `opencode.cmd`. Relay
launches the command directly rather than through a shell, and a bare `opencode` only finds
`opencode.exe` — not the `.cmd` shim that npm-installed CLIs put on `PATH`.

### Dynamic Relay Configuration

The `dynamic` section is written and managed by the `verdalia-relay dynamic pair` command. It should not be edited manually.

```json
{
  "dynamic": {
    "enabled": true,
    "api_base_url": "https://api.verdalia.io",
    "websocket_url": "wss://api.verdalia.io/ws/relay",
    "relay_identity_id": "01J...",
    "connection_id": "01J...",
    "workspace_id": "01J...",
    "machine_id": "01J...",
    "private_key_path": "/home/user/.config/verdalia-relay/dynamic-relay-ed25519.key"
  }
}
```

When `enabled: true`, all other fields must be populated. `api_base_url` must use `https` unless the host is `localhost`.

## Connection Modes

Relay has two connection modes:

| Mode | When to use |
|------|-------------|
| **Static** | Your Relay has a stable HTTPS URL (VPS, reverse-proxied host). Canopy sends signed HTTP requests to that URL. |
| **Dynamic** | Your Relay runs on a laptop, desktop, or home server behind NAT. Relay opens an outbound WebSocket connection to Canopy. |

### Static Mode Setup

1. Configure and start Relay on your host.
2. Expose it through a stable HTTPS URL (direct TLS, Nginx reverse proxy, Cloudflare Tunnel, or ngrok).
3. In Canopy, open **Settings → Connections** and create a **Verdalia Relay** connection.
4. Choose **Static**.
5. Enter the Relay URL and API key.
6. Save and test the connection.

Static mode uses signed HTTP requests. Each request includes:

- `Authorization: Bearer <api_key>` — API key with `relay_` prefix
- `X-Relay-Signature` — Base64 Ed25519 signature
- `X-Relay-Timestamp` — Unix timestamp (within 5-minute window)
- `X-Relay-Nonce` (optional) — Per-request nonce for replay protection

The canonical signed payload is:

```
<METHOD>\n<path>\n<query>\n<timestamp>\n[<nonce>\n]<body_sha256_hex>
```

### Dynamic Mode Setup

1. Install `verdalia-relay` on the target machine.
2. In Canopy, open **Settings → Connections** and create a **Verdalia Relay** connection.
3. Choose **Dynamic**.
4. Save the connection, then click **Generate pairing command**.
5. Run the pairing command shown in Canopy:

   ```bash
   verdalia-relay dynamic pair <pairing-code> --api-base-url https://api.verdalia.io
   ```

   This generates an Ed25519 keypair saved to `~/.config/verdalia-relay/dynamic-relay-ed25519.key`, sends the public key and pairing code to Canopy, and writes the `dynamic` config section.

6. Start Relay and leave it running:

   ```bash
   verdalia-relay serve
   ```

7. Wait for the connection status in Canopy to show **Online**.

Once online, the Relay maintains a persistent WebSocket connection to Canopy's broker. If the local process stops or the machine sleeps, requests fail instead of being queued.

## CLI Reference

```
verdalia-relay <SUBCOMMAND>

Subcommands:
  serve     Start the HTTP server
  key       Manage API keys
  dynamic   Manage dynamic relay pairing
  hue       Manage the paired Hue Bridge and its lights
  status    Show relay configuration, plugins, and paired state
  config    Read and change relay settings; opens an editor when run in a terminal
  help      Print help
```

### `verdalia-relay serve`

Starts the Relay HTTP server.

```bash
verdalia-relay serve [--host <HOST>] [--port <PORT>]
```

| Flag | Overrides config |
|------|-----------------|
| `--host` | `server.host` |
| `--port` | `server.port` |

### `verdalia-relay key`

Manage API keys for authentication.

```bash
# Create a new API key
verdalia-relay key create [--name <NAME>] [--public-key-stdin] [--format <table|json>]

# List existing key bindings
verdalia-relay key list [--format <table|json>]

# Revoke a key binding by ID
verdalia-relay key revoke <ID>
```

Key management commands require user confirmation before making changes. The API key (prefix `relay_`) is shown once at creation time and cannot be retrieved later.

**`key create`**: Creates a new authorized key binding. Prompts for the Canopy public key (or reads from stdin with `--public-key-stdin`). The Canopy public key is available in your workspace under **Settings → Connections → Verdalia Relay**.

### `verdalia-relay dynamic`

```bash
# Pair with Canopy
verdalia-relay dynamic pair <PAIRING-CODE> --api-base-url <URL> [--label <LABEL>] [--format <human|json>]

# Show pairing status
verdalia-relay dynamic status
```

## Hue Bridge

Pairing and light control live on the command line. Nothing here needs `serve` to be running,
and none of it goes through Canopy.

```bash
# Find a bridge, press its link button when prompted, and store the key
verdalia-relay hue pair

# Show the paired bridge and whether it answers
verdalia-relay hue status

# Lights, rooms, and scenes
verdalia-relay hue lights
verdalia-relay hue light <LIGHT-ID> --on --brightness 0.4
verdalia-relay hue light <LIGHT-ID> --kelvin 2700
verdalia-relay hue all off
verdalia-relay hue groups
verdalia-relay hue group <GROUP-ID> on
verdalia-relay hue scenes
verdalia-relay hue scene <SCENE-ID>

# Forget the bridge
verdalia-relay hue unpair
```

`hue pair` searches the network for bridges, asks which one to use when it finds several, and
then waits for the link button — retrying six times, five seconds apart. Pass `--ip <ADDRESS>`
to skip discovery. Every listing command takes `--format json`.

`hue light` with no state flags prints the light's current state instead of changing it. When
`--brightness`, `--kelvin`, or `--rgb` is given without `--on` or `--off`, the light keeps its
current switch.

## Relay Status

```bash
verdalia-relay status [--format <human|json>]
```

Shows the config and state paths, the API address and whether something is listening on it,
whether each plugin is on, off, or waiting for a mount, dynamic pairing state, the paired Hue
Bridge, and how many API keys exist.

## Service Setup

### systemd (Linux)

Relay reads its config, state, and Hue credentials from your home directory, so on a laptop or
desktop it belongs in the **user** manager, where it runs as you.

**1. Create `~/.config/systemd/user/verdalia-relay.service`.**

```ini
[Unit]
Description=Verdalia Relay

[Service]
Type=simple
ExecStart=%h/.local/bin/verdalia-relay serve
Restart=always
RestartSec=10
Environment=RUST_LOG=info

[Install]
WantedBy=default.target
```

**Note**: directive names are case-sensitive. `execstart=` is discarded as an unknown key and the
unit then refuses to load with `Service has no ExecStart=`.

**Coding agents:** a user service does not read your shell profile, so its `PATH` is usually only
`/usr/local/bin:/usr/bin`. An agent installed under your home directory, such as
`~/.local/bin/omp`, is then not found, and starting a session says so. Give the agent's full path:

```bash
verdalia-relay config set opencode_acp.omp.command ~/.local/bin/omp   # or opencode_acp.opencode.command
```

**2. Enable and start it.**

```bash
systemctl --user daemon-reload
systemctl --user enable --now verdalia-relay
systemctl --user status verdalia-relay
```

The user manager starts at login and stops at logout, taking Relay with it. To run it from boot
without logging in:

```bash
loginctl enable-linger $USER
```

Relay logs to stderr only, so journald holds them and `RUST_LOG` in the unit sets the level. There
is no log file.

```bash
journalctl --user -u verdalia-relay -f
```

#### As a system service

On a host nobody logs into — a VPS or home server — install it system-wide instead and name the
user it runs as. Config and state come from that user's home, so pair it with the account that ran
`dynamic pair` and `hue pair`.

Create `/etc/systemd/system/verdalia-relay.service`:

```ini
[Unit]
Description=Verdalia Relay
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=your-user
ExecStart=/usr/local/bin/verdalia-relay serve
Restart=always
RestartSec=10
Environment=RUST_LOG=info

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now verdalia-relay
```

### Task Scheduler (Windows)

Relay is a console program, so the way to run it in the background at login is a scheduled task.
From an elevated PowerShell, with the path to where you unpacked it:

```powershell
$exe = "C:\Tools\verdalia-relay\verdalia-relay.exe"
$action    = New-ScheduledTaskAction -Execute $exe -Argument "serve"
$trigger   = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
$settings  = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
                                          -ExecutionTimeLimit 0 -AllowStartIfOnBatteries
Register-ScheduledTask -TaskName "Verdalia Relay" -Action $action -Trigger $trigger `
                       -Settings $settings -RunLevel Limited
```

`-RunLevel Limited` matters: Relay reads its config and Hue credentials from `%APPDATA%`, so it
must run as you rather than as SYSTEM, which has a different profile and would not find them.
`-ExecutionTimeLimit 0` stops Windows killing a long-running task after three days.

```powershell
Start-ScheduledTask -TaskName "Verdalia Relay"
Get-ScheduledTaskInfo -TaskName "Verdalia Relay"
```

Relay logs to stderr and nothing else, and a scheduled task discards that. To keep logs, point the
task at a wrapper that redirects them:

```powershell
$action = New-ScheduledTaskAction -Execute "cmd.exe" `
    -Argument "/c `"$exe`" serve >> `"$env:LOCALAPPDATA\verdalia-relay.log`" 2>&1"
```

## Networking

### Nginx Reverse Proxy (Static Mode)

```nginx
server {
    listen 443 ssl;
    server_name relay.your-domain.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
```

### Cloudflare Tunnel

```bash
cloudflared tunnel --url http://localhost:8080
```

### ngrok

```bash
ngrok http 8080
```

## Environment Variables

| Variable | Description |
|----------|-------------|
| `RUST_LOG` | Log level filter. Default: `info` for `serve`, `warn` for every other command. |

All other configuration is in `~/.config/verdalia-relay/config.json`; see [Changing settings](#changing-settings).

## Identity Database

Relay stores state in an SQLite database at `~/.config/verdalia-relay/state.db`. This includes API key bindings with Ed25519 public key hashes and Hue Bridge credentials.

## Use Relay in Workflows and Tools

After the connection is saved, select it anywhere Canopy asks for a Relay connection:

- Relay Request workflow node
- Relay Filesystem workflow node
- Relay ACP workflow node
- Hue Light Control workflow node
- Relay filesystem assistant tools

The node or tool does not need to know whether the connection is static or dynamic. Choose the connection, configure the action, and run it.

## Troubleshooting

**Dynamic Relay says unpaired**

Create a new pairing code in Canopy and run the pairing command again on the local machine.

**Dynamic Relay is offline**

Make sure `verdalia-relay serve` is still running and the machine has network access to Canopy. Check the log output with `RUST_LOG=debug`.

**A request fails after the machine sleeps**

Start Relay again and wait for the connection to return online. Dynamic Relay does not queue requests while offline.

**Static Relay fails to connect**

Check that the URL is reachable from Canopy, TLS is valid, and the API key matches the key configured in Relay.

**Canopy public key is rejected during key creation**

The public key shown in your Canopy workspace under **Connections → Verdalia Relay** must be entered exactly as displayed. Use `--public-key-stdin` to pipe it directly.
