subduction_cli 0.3.0

CLI server and client for Subduction sync over WebSockets
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
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
# 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 provides multiple server modes:

- **`server`** - Subduction document sync server (persistent CRDT storage)
- **`client`** - Subduction client connecting to a server
- **`ephemeral-relay`** - Simple relay for ephemeral messages (presence, awareness)

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

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

## Commands

### Server Mode

Start a Subduction server for document synchronization:

```bash
# With Nix
nix run .#subduction_cli -- server --socket 0.0.0.0:8080

# With Cargo
cargo run --release -- server --socket 0.0.0.0:8080
```

Options:
- `--socket <ADDR>` - Socket address to bind to (default: `0.0.0.0:8080`)
- `--data-dir <PATH>` - Data directory for storage (default: `./data`)
- `--peer-id <ID>` - Peer ID as 64 hex characters (default: auto-generated)
- `--timeout <SECS>` - Request timeout in seconds (default: `5`)
- `--peer <URL>` - Peer WebSocket URL to connect to on startup (can be specified multiple times)
- `--metrics` - Enable Prometheus metrics server (disabled by default)
- `--metrics-port <PORT>` - Port for Prometheus metrics endpoint (default: `9090`, only used if `--metrics` is enabled)

### Client Mode

Connect as a client to a Subduction server:

```bash
# With Nix
nix run .#subduction_cli -- client --server ws://127.0.0.1:8080

# With Cargo
cargo run --release -- client --server ws://127.0.0.1:8080
```

Options:
- `--server <URL>` - WebSocket server URL to connect to
- `--data-dir <PATH>` - Data directory for local storage (default: `./client-data`)
- `--peer-id <ID>` - Peer ID as 64 hex characters (default: auto-generated)
- `--timeout <SECS>` - Request timeout in seconds (default: `5`)

### Ephemeral Relay Mode

Start a relay server for ephemeral messages (presence, awareness):

```bash
# With Nix
nix run .#subduction_cli -- ephemeral-relay --socket 0.0.0.0:8081

# With Cargo
cargo run --release -- ephemeral-relay --socket 0.0.0.0:8081
```

Alias: `relay`

Options:
- `--socket <ADDR>` - Socket address to bind to (default: `0.0.0.0:8081`)
- `--max-message-size <BYTES>` - Maximum message size in bytes (default: `1048576` = 1 MB)

#### Architecture

The ephemeral relay server provides a simple broadcast mechanism for ephemeral messages like presence, awareness, cursor positions, etc.

```
┌────────────────────────────────────────┐
│      Client (e.g. automerge-repo)      │
│  ┌──────────────┐    ┌──────────────┐  │
│  │   WS :8080   │    │   WS :8081   │  │
│  │ (subduction) │    │ (ephemeral)  │  │
│  └──────┬───────┘    └──────┬───────┘  │
└─────────┼───────────────────┼──────────┘
          │                   │
          ▼                   ▼
   ┌──────────────┐    ┌──────────────┐
   │ Subduction   │    │ Ephemeral    │
   │ Server       │    │ Relay Server │
   │ Port 8080    │    │ Port 8081    │
   └──────────────┘    └──────────────┘
    Document Sync     Presence/Awareness
    (persistent)         (ephemeral)
```

#### How It Works

**Subduction Server (default port 8080)**
- Handles document synchronization
- Persists changes to storage
- Uses Subduction protocol (CBOR-encoded Messages)
- For CRDTs, fragments, commits, batch sync

**Ephemeral Relay (default port 8081)**
- Implements automerge-repo NetworkSubsystem protocol handshake
- Responds to "join" messages with "peer" messages
- Broadcasts ephemeral messages between connected peers
- Does NOT persist messages
- For presence, awareness, cursors, temporary state
- Uses sharded deduplication with AHash for DoS-resistant message filtering

#### Client Configuration

In your automerge-repo client:

```typescript
const repo = new Repo({
  network: [
    // Document sync via Subduction
    new WebSocketClientAdapter("ws://127.0.0.1:8080", 5000, { subductionMode: true }),

    // Ephemeral messages via relay server
    new WebSocketClientAdapter("ws://127.0.0.1:8081"),
  ],
  subduction: await Subduction.hydrate(db),
})
```

#### Message Flow

**Document Changes**
```
Client → WebSocket:8080 → Subduction Server → Storage
                    Other Clients
```

**Presence Updates**
```
Client → WebSocket:8081 → Relay Server → Other Clients
                         (broadcast)
```

#### Benefits

- **Clean separation**: Document sync and ephemeral messages use different protocols
- **No Subduction changes**: Relay server is independent
- **Simple relay**: Just broadcasts messages, no processing
- **Stateless**: Relay server doesn't persist anything
- **Scalable**: Can run relay on different machine/port as needed
- **DoS-resistant**: Sharded deduplication prevents duplicate message floods

#### Production Considerations

For production use, you might want to:

1. **Add authentication** - Verify peer identities
2. **Add rate limiting** - Prevent spam
3. **Add targeted relay** - Parse targetId and relay specifically (vs broadcast)
4. **Add metrics** - Track connections, message rates
5. **Use single port** - Multiplex both protocols on one WebSocket (more complex)
6. **Add message authentication** - Prevent forged ephemeral messages (see code TODOs)
7. **Add timestamp validation** - Prevent replay attacks (see code TODOs)

## Typical Setup

For a complete setup supporting both document sync and presence:

**Terminal 1: Document Sync Server**
```bash
nix run .#subduction_cli -- server --socket 0.0.0.0:8080
```

**Terminal 2: Ephemeral Relay Server**
```bash
nix run .#subduction_cli -- relay --socket 0.0.0.0:8081
```

Your clients can then connect to:
- Port 8080 for document synchronization
- Port 8081 for ephemeral messages (presence, awareness, etc.)

## Environment Variables

- `RUST_LOG` - Set log level (e.g., `RUST_LOG=debug`)
- `TOKIO_CONSOLE` - Enable tokio console for debugging async tasks

## Examples

```bash
# Server with debug logging
RUST_LOG=debug nix run .#subduction_cli -- server

# Server connecting to peers on startup for bidirectional sync
nix run .#subduction_cli -- server --peer ws://192.168.1.100:8080 --peer ws://192.168.1.101:8080

# Server with metrics enabled
nix run .#subduction_cli -- server --metrics --metrics-port 9090

# Client connecting to remote server
nix run .#subduction_cli -- client --server ws://sync.example.com:8080

# Ephemeral relay on custom port
nix run .#subduction_cli -- relay --socket 0.0.0.0:9000

# Ephemeral relay with 5 MB message size limit
nix run .#subduction_cli -- relay --max-message-size 5242880
```

<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 = {
            # Document sync server
            server = {
              enable = true;
              socket = "0.0.0.0:8080";
              dataDir = "/var/lib/subduction";
              timeout = 5;
              # peerId = "...";  # optional: 64 hex chars

              # Connect to other peers on startup for bidirectional sync
              peers = [
                "ws://192.168.1.100:8080"
                "ws://192.168.1.101:8080"
              ];

              # Prometheus metrics (disabled by default)
              enableMetrics = true;
              metricsPort = 9090;
            };

            # Ephemeral message relay
            relay = {
              enable = true;
              socket = "0.0.0.0:8081";
              maxMessageSize = 1048576;  # 1 MB
            };

            # Shared settings
            user = "subduction";
            group = "subduction";
            openFirewall = true;  # opens server + relay ports
          };
        }
      ];
    };
  };
}
```

This creates two systemd services:
- `subduction.service` - Document sync server
- `subduction-relay.service` - Ephemeral message relay

Manage with:
```bash
systemctl status subduction
systemctl status subduction-relay
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";
              # dataDir defaults to ~/.local/share/subduction

              # Connect to other peers on startup
              peers = ["ws://sync.example.com:8080"];
            };

            relay = {
              enable = true;
              socket = "127.0.0.1:8081";
            };
          };
        }
      ];
    };
  };
}
```

On Linux, manage with:
```bash
systemctl --user status subduction
systemctl --user status subduction-relay
```

On macOS, manage with:
```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";
  };
  relay = {
    enable = true;
    socket = "127.0.0.1:8081";
  };
  openFirewall = false;  # Caddy handles external access
};

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

Caddy automatically handles WebSocket upgrades and TLS certificates.

</details>

## Monitoring

The Subduction server exposes Prometheus metrics on a configurable port (default: `9090`).

### Server Metrics Options

```bash
# Enable metrics
subduction_cli server --metrics --metrics-port 9090

# Metrics are disabled by default
subduction_cli server
```

### Development Monitoring Stack

When developing locally with Nix, use the `monitoring:start` command to launch Prometheus and Grafana with pre-configured dashboards:

```bash
# Enter the dev shell
nix develop

# Start the monitoring stack
monitoring:start
```

This starts:
- **Prometheus** at `http://localhost:9092` - scrapes metrics from the server
- **Grafana** at `http://localhost:3939` - pre-configured dashboards

The Grafana dashboard includes panels for connections, messages, sync operations, and storage.

### Production Monitoring

For production, configure your Prometheus instance to scrape the metrics endpoint:

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

Import the Grafana dashboard from `subduction_cli/monitoring/grafana/provisioning/dashboards/subduction.json`.