subduction_cli 0.6.0

CLI server for Subduction sync over WebSocket, HTTP long-poll, and Iroh (QUIC)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# Subduction CLI

> [!CAUTION]
> This is an early release preview. It has a very unstable API. No guarantees are given. DO NOT use for production use cases at this time. USE AT YOUR OWN RISK.

## Overview

The Subduction CLI runs a document sync server with three transport layers:

- **WebSocket** — browser-compatible, enabled by default
- **HTTP Long Poll** — fallback for restrictive networks, enabled by default
- **Iroh (QUIC)** — NAT-traversing P2P via [iroh]https://iroh.computer, opt-in

## Installation

<details>
<summary><h3>Using Nix</h3></summary>

```bash
# Run directly without installing
nix run github:inkandswitch/subduction -- --help

# Install to your profile
nix profile install github:inkandswitch/subduction

# Then run
subduction_cli server --socket 0.0.0.0:8080 --ephemeral-key
```

#### Adding to a Flake

```nix
{
  inputs.subduction.url = "github:inkandswitch/subduction";

  outputs = { nixpkgs, subduction, ... }: {
    # NixOS
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      modules = [{
        environment.systemPackages = [
          subduction.packages.x86_64-linux.default
        ];
      }];
    };

    # Home Manager
    homeConfigurations.myuser = home-manager.lib.homeManagerConfiguration {
      modules = [{
        home.packages = [
          subduction.packages.x86_64-linux.default
        ];
      }];
    };
  };
}
```

</details>

### Using Cargo

```bash
# Build from source
cargo build --release

# Run
./target/release/subduction_cli --help
```

## Key Management

The server requires an **Ed25519 signing key seed** (32 bytes). The seed is used to deterministically derive both the signing key and the public verifying key. The verifying key becomes the server's **peer ID**.

### Generating a Key

Any source of 32 cryptographically random bytes works. The CLI accepts either 64 hex characters or 32 raw bytes.

```bash
# OpenSSL
openssl rand -hex 32

# /dev/urandom
head -c 32 /dev/urandom | xxd -p -c 64

# Python
python3 -c "import secrets; print(secrets.token_hex(32))"
```

### Providing the Key

| Flag | Description |
|------|-------------|
| `--key-seed <HEX>` | 64 hex characters on the command line |
| `--key-file <PATH>` | Path to a file containing 64 hex characters or 32 raw bytes |
| `--ephemeral-key` | Generate a random key (lost on restart) |

These are mutually exclusive. Exactly one must be provided.

`--key-file` is recommended for production. The file must contain either:
- 64 hex characters (with optional trailing newline), or
- Exactly 32 raw bytes

```bash
# Create a persistent key file
openssl rand -hex 32 > /var/lib/subduction/key
chmod 600 /var/lib/subduction/key

# Start with key file
subduction_cli server --key-file /var/lib/subduction/key
```

> [!WARNING]
> The key seed is equivalent to a private key. Do not commit it to version control or expose it in logs.

## Commands

### `server`

Start a Subduction sync node.

```bash
subduction_cli server --socket 0.0.0.0:8080 --key-file ./key
```

#### General Options

| Flag | Default | Description |
|------|---------|-------------|
| `-s, --socket <ADDR>` | `0.0.0.0:8080` | Socket address to bind to |
| `-d, --data-dir <PATH>` | `./data` | Data directory for filesystem storage |
| `-t, --timeout <SECS>` | `5` | Request timeout in seconds |
| `--handshake-max-drift <SECS>` | `600` | Maximum clock drift allowed during handshake |
| `--service-name <NAME>` | socket address | Service name for discovery mode handshake |
| `--max-message-size <BYTES>` | `52428800` (50 MB) | Maximum WebSocket message size |

#### Transport Options

| Flag | Default | Description |
|------|---------|-------------|
| `--websocket` | enabled | Enable the WebSocket transport |
| `--longpoll` | enabled | Enable the HTTP long-poll transport |
| `--iroh` | disabled | Enable the Iroh (QUIC) transport |

At least one transport must be enabled.

#### WebSocket Peer Options

| Flag | Description |
|------|-------------|
| `--ws-peer <URL>` | WebSocket peer URL to connect to on startup (repeatable) |

#### Iroh Peer Options

| Flag | Description |
|------|-------------|
| `--iroh-peer <NODE_ID>` | Iroh peer node ID to connect to (z32-encoded, repeatable) |
| `--iroh-peer-addr <IP:PORT>` | Direct address hint for iroh peers (repeatable) |
| `--iroh-direct-only` | Skip relay servers, direct connections only |
| `--iroh-relay-url <URL>` | Route through a specific relay instead of the public default |

By default, iroh routes traffic through [iroh's public relay infrastructure](https://iroh.computer) for NAT traversal. Use `--iroh-direct-only` for LAN-only deployments, or `--iroh-relay-url` to point at a self-hosted [`iroh-relay`](https://docs.rs/iroh-relay) instance.

#### Metrics Options

| Flag | Default | Description |
|------|---------|-------------|
| `--metrics` | disabled | Enable the Prometheus metrics server |
| `--metrics-port <PORT>` | `9090` | Port for Prometheus metrics endpoint |
| `--metrics-refresh-interval <SECS>` | `60` | Interval for refreshing storage metrics |

#### Other Options

| Flag | Description |
|------|-------------|
| `--ready-file <PATH>` | Write a file on startup with assigned port, peer ID, and iroh node ID |

### `purge`

Delete all stored data.

```bash
subduction_cli purge --data-dir ./data
```

| Flag | Default | Description |
|------|---------|-------------|
| `-d, --data-dir <PATH>` | `./data` | Data directory to purge |
| `-y, --yes` | | Skip confirmation prompt |

## Examples

```bash
# Minimal server with an ephemeral key
subduction_cli server --ephemeral-key

# Server with persistent key and custom data directory
subduction_cli server --key-file ./key --data-dir /var/lib/subduction

# Two WebSocket servers syncing bidirectionally
subduction_cli server --key-file ./key1 --socket 0.0.0.0:8080 \
  --ws-peer ws://192.168.1.101:8080
subduction_cli server --key-file ./key2 --socket 0.0.0.0:8080 \
  --ws-peer ws://192.168.1.100:8080

# Iroh P2P (NAT-traversing, no WebSocket peers needed)
subduction_cli server --key-file ./key --iroh \
  --iroh-peer <remote-node-id>

# Iroh direct only (LAN, no relay)
subduction_cli server --key-file ./key --iroh --iroh-direct-only \
  --iroh-peer <node-id> --iroh-peer-addr 192.168.1.50:12345

# Discovery mode (clients connect by service name instead of peer ID)
subduction_cli server --key-file ./key --service-name sync.example.com

# Enable Prometheus metrics
subduction_cli server --key-file ./key --metrics --metrics-port 9090

# Debug logging
RUST_LOG=debug subduction_cli server --ephemeral-key
```

<details>
<summary><h2>Running as a System Service</h2></summary>

The flake provides NixOS and Home Manager modules for running Subduction as a managed service.

### NixOS (systemd)

```nix
{
  inputs.subduction.url = "github:inkandswitch/subduction";

  outputs = { nixpkgs, subduction, ... }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        subduction.nixosModules.default
        {
          services.subduction = {
            server = {
              enable = true;
              socket = "0.0.0.0:8080";
              dataDir = "/var/lib/subduction";
              keyFile = "/var/lib/subduction/key";
              timeout = 5;

              # WebSocket peers for bidirectional sync
              wsPeers = [
                "ws://192.168.1.100:8080"
                "ws://192.168.1.101:8080"
              ];

              # Iroh P2P transport
              iroh = {
                enable = true;
                peers = ["<remote-node-id>"];
                # directOnly = true;      # LAN only, no relay
                # relayUrl = "https://..."; # self-hosted relay
              };

              # Prometheus metrics
              enableMetrics = true;
              metricsPort = 9090;
            };

            # Shared settings
            user = "subduction";
            group = "subduction";
            openFirewall = true;
          };
        }
      ];
    };
  };
}
```

This creates a systemd service: `subduction.service`

```bash
systemctl status subduction
journalctl -u subduction -f
```

### Home Manager (user service)

Works on both Linux (systemd user service) and macOS (launchd agent):

```nix
{
  inputs.subduction.url = "github:inkandswitch/subduction";

  outputs = { home-manager, subduction, ... }: {
    homeConfigurations.myuser = home-manager.lib.homeManagerConfiguration {
      modules = [
        subduction.homeManagerModules.default
        {
          services.subduction = {
            server = {
              enable = true;
              socket = "127.0.0.1:8080";
              keyFile = "/home/myuser/.config/subduction/key";
              # dataDir defaults to ~/.local/share/subduction

              wsPeers = ["ws://sync.example.com:8080"];

              iroh.enable = true;
            };
          };
        }
      ];
    };
  };
}
```

On Linux:
```bash
systemctl --user status subduction
```

On macOS:
```bash
launchctl list | grep subduction
tail -f ~/.cache/subduction/server.log
```

### Behind a Reverse Proxy (Caddy)

When running behind Caddy or another reverse proxy, bind to localhost:

```nix
services.subduction.server = {
  enable = true;
  socket = "127.0.0.1:8080";
  keyFile = "/var/lib/subduction/key";
};

services.caddy = {
  enable = true;
  virtualHosts."sync.example.com".extraConfig = ''
    reverse_proxy localhost:8080
  '';
};
```

Caddy automatically handles WebSocket upgrades and TLS certificates.

</details>

## Monitoring

### Prometheus

Enable the metrics endpoint:

```bash
subduction_cli server --key-file ./key --metrics --metrics-port 9090
```

Configure Prometheus to scrape:

```yaml
# prometheus.yml
scrape_configs:
  - job_name: 'subduction'
    static_configs:
      - targets: ['localhost:9090']
```

### Development Monitoring Stack

With Nix, use the built-in command to launch Loki, Prometheus, and Grafana with pre-configured datasources and dashboards:

```bash
nix develop
monitoring:start
```

This starts:
- **Loki** at `http://localhost:3100` (log aggregation)
- **Prometheus** at `http://localhost:9092` (metrics)
- **Grafana** at `http://localhost:3939` with pre-configured datasources and dashboards

To ship logs from the Subduction server to Loki, run it with `LOKI_URL`:

```bash
LOKI_URL=http://localhost:3100 cargo run -p subduction_cli -- server
```

Then query logs in Grafana via the Explore tab with the Loki datasource:

```
{service="subduction"} |= ""
```

### Grafana Dashboard

Import the dashboard from `subduction_cli/monitoring/grafana/provisioning/dashboards/subduction.json`. It includes panels for connections, messages, sync operations, and storage.

## Environment Variables

| Variable | Description |
|----------|-------------|
| `RUST_LOG` | Log level filter for console output (e.g. `debug`, `info`, `subduction_core=trace`) |
| `LOKI_URL` | Grafana Loki endpoint for log shipping (e.g. `http://localhost:3100`) |
| `LOKI_LOG` | Log level filter for Loki (default: `info`). Same syntax as `RUST_LOG`. |
| `LOKI_SERVICE_NAME` | Service label for Loki (default: `subduction`) |
| `TOKIO_CONSOLE` | Set to any value to enable [tokio-console]https://github.com/tokio-rs/console |