> ## Documentation Index
> Fetch the complete documentation index at: https://crosslink.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-Hosting

> Running your own signaling and relay servers

## Overview

Crosslink's signaling nodes are horizontally scalable with Redis. Relay channels are
ephemeral and process-local, with region catalogs and host-side fallback for scale-out.

## Quick start

```bash theme={null}
git clone https://github.com/jacobpowaza/crosslink.git
cd crosslink
npm install
npm run stack
```

This starts both services on localhost:

* Signaling: `ws://127.0.0.1:8081`
* Relay: `http://127.0.0.1:8082`

## Services

### Signaling service

The signaling service helps devices find each other and route pairing codes.

**What it sees:**

* Hashed pairing codes
* Opaque signed blobs (not readable without private keys)
* Device fingerprints (for routing)

**What it cannot see:**

* Plaintext content
* Session keys
* Private keys

### Relay service

The relay service forwards encrypted traffic when a direct connection isn't possible.

**What it sees:**

* Ciphertext (encrypted frames)

**What it cannot see:**

* Plaintext content
* Session keys
* Message content

## Building your own container image

Crosslink does not publish Docker images or Dockerfiles today. Each service is a plain Node.js CLI (`@crosslink/signaling`'s `crosslink-signaling` bin, `@crosslink/relay`'s `crosslink-relay` bin) built with `npm run build`, so it packages into a container however you already build Node services -- copy `services/signaling` (or `services/relay`) and its built `dist/` into a `node:20-slim` (or similar) base image and run the bin.

Both services require a bind before phones can reach them from anywhere but the machine they're on; use your normal container port-mapping.

## Environment variables

### Signaling

| Variable                    | Default   | Description                                                                          |
| --------------------------- | --------- | ------------------------------------------------------------------------------------ |
| `PORT`                      | `8081`    | HTTP/WebSocket server port                                                           |
| `HOST`                      | `0.0.0.0` | Bind address                                                                         |
| `CROSSLINK_SIGNALING_TOKEN` | *(none)*  | Shared secret hosts must present; required unless per-machine dev tokens apply       |
| `CROSSLINK_REGION`          | *(none)*  | Optional region label included in presence data                                      |
| `CROSSLINK_REDIS_URL`       | *(none)*  | Redis URL for shared TTL state and cross-node pub/sub; use `rediss://` in production |

### Relay

| Variable                        | Default       | Description                                                                    |
| ------------------------------- | ------------- | ------------------------------------------------------------------------------ |
| `PORT`                          | `8082`        | HTTP/WebSocket server port                                                     |
| `HOST`                          | `0.0.0.0`     | Bind address                                                                   |
| `CROSSLINK_RELAY_TOKEN`         | *(none)*      | Shared secret hosts must present; required unless per-machine dev tokens apply |
| `CROSSLINK_RELAY_CLIENT_TOKEN`  | *(none)*      | Optional shared secret clients must present to attach                          |
| `CROSSLINK_RELAY_MAX_CLIENTS`   | *(unset)*     | Max clients per relay channel                                                  |
| `CROSSLINK_RELAY_MAX_CHANNELS`  | `1024`        | Max allocated channels per process                                             |
| `CROSSLINK_RELAY_MAX_BYTES`     | *(unlimited)* | Lifetime traffic cap per channel                                               |
| `CROSSLINK_RELAY_BYTES_PER_SEC` | *(unlimited)* | Token-bucket rate per channel                                                  |
| `CROSSLINK_RELAY_PUBLIC_URL`    | *(none)*      | Public HTTPS URL for this relay region                                         |
| `CROSSLINK_RELAY_REGIONS`       | *(none)*      | JSON array of regional URLs and priorities                                     |

<Warning>
  Outside of local development, both services refuse to start without an auth token (`CROSSLINK_SIGNALING_TOKEN` / `CROSSLINK_RELAY_TOKEN`) or the per-machine dev tokens written to `.crosslink-data/dev-tokens.json`. Set these explicitly for any deployment other services will connect to.
</Warning>

## Production considerations

### TLS termination

Use a reverse proxy (nginx, Caddy, Cloudflare) for TLS:

```nginx theme={null}
# nginx.conf
server {
    listen 443 ssl;
    server_name signaling.example.com;

    ssl_certificate /etc/letsencrypt/live/signaling.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/signaling.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8081;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}
```

### Load balancing

Signaling can be horizontally scaled when every replica uses the same Redis
deployment. Relay WebSockets require channel affinity; deploy independent regional
relay pools and let hosts allocate using ordered regional fallback:

```nginx theme={null}
upstream signaling {
    server 10.0.0.1:8081;
    server 10.0.0.2:8081;
    server 10.0.0.3:8081;
}

upstream relay {
    server 10.0.0.1:8082;
    server 10.0.0.2:8082;
    server 10.0.0.3:8082;
}
```

<Warning>
  Do not round-robin an allocated relay channel across processes. Either keep
  WebSocket affinity for the channel or publish each relay pool as a distinct regional
  allocation endpoint.
</Warning>

### Monitoring

Monitor these metrics:

| Metric           | Warning threshold                               |
| ---------------- | ----------------------------------------------- |
| Connection count | Sustained growth with no corresponding drop-off |
| Memory usage     | > 80% of container limit                        |
| Error rate       | > 1% of requests                                |
| Latency (p99)    | > 500ms                                         |

### Backup

Relay channel state and signaling presence are ephemeral. Redis persistence is not
required for correctness after a restart, but production configuration must be
recoverable. Back up:

* Environment variables (including auth tokens)
* TLS certificates
* Your deployment manifests

## Security checklist

* [ ] Use TLS termination (nginx, Caddy, Cloudflare)
* [ ] Use `rediss://`, Redis ACLs, and a private Redis network
* [ ] Set channel, client, lifetime-byte, bandwidth, and edge connection quotas
* [ ] Run in a DMZ or private network
* [ ] Monitor connection counts and error rates
* [ ] Set `CROSSLINK_SIGNALING_TOKEN` / `CROSSLINK_RELAY_TOKEN` explicitly
* [ ] Enable health checks
* [ ] Keep secrets out of source control (env vars, a secrets manager, or Docker/Kubernetes secrets)
* [ ] Regularly update the packages
* [ ] Restrict network access with firewall rules

## Custom deployment

### Kubernetes

```yaml theme={null}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: crosslink-signaling
spec:
  replicas: 3
  selector:
    matchLabels:
      app: crosslink-signaling
  template:
    metadata:
      labels:
        app: crosslink-signaling
    spec:
      containers:
      - name: signaling
        image: your-registry/crosslink-signaling:latest  # built per "Building your own container image" above
        ports:
        - containerPort: 8081
        env:
        - name: PORT
          value: "8081"
        - name: CROSSLINK_SIGNALING_TOKEN
          valueFrom:
            secretKeyRef:
              name: crosslink-signaling
              key: token
        resources:
          limits:
            memory: "256Mi"
            cpu: "500m"
```

### Systemd service

```ini theme={null}
[Unit]
Description=Crosslink Signaling Service
After=network.target

[Service]
Type=simple
User=crosslink
Environment=PORT=8081
Environment=CROSSLINK_SIGNALING_TOKEN=change-me
ExecStart=/usr/bin/node /opt/crosslink/signaling/dist/cli.js
Restart=always
RestartSec=5

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