# crates/relay reference

> Reference for the code under `crates/relay`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Repository: egoist/lorca
- GitHub: https://github.com/egoist/lorca
- Human docs: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6
- Complete Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/llms-full.txt

## Source Files

- `crates/relay/src/main.rs`
- `crates/relay/src/routes.rs`

---

---
title: "crates/relay reference"
description: "Reference for the code under `crates/relay`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

Run the binary in one command:

```bash
cargo run -q -p lorca-relay -- --bind 127.0.0.1:8787 --db lorca-relay.db
```

`crates/relay` is the **binary-only** crate `lorca-relay`: a zero-knowledge store-and-forward HTTP + WebSocket service. It keeps identity/machine public keys and opaque ciphertext, issues HMAC bearer tokens after an Ed25519 challenge, and never decrypts payloads. There is no `lib.rs`. The public surface is the `lorca-relay` binary (see `crates/relay/Cargo.toml` `[[bin]]`) and the HTTP API wired in `crates/relay/src/main.rs` and `crates/relay/src/routes.rs`.

## Invoke

| Path | Command / signal |
| --- | --- |
| Local binary | `cargo run -p lorca-relay -- [flags]` or `target/debug/lorca-relay` / `target/release/lorca-relay` |
| Repo npm script | `bun run relay` → `mkdir -p temp`, then bind `0.0.0.0:8787`, `--db temp/lorca-relay.db`, `--apns-topic app.lorca.dev` |
| Docker | `docker build -f crates/relay/Dockerfile -t lorca-relay .` then `docker run -p 8787:8787 -v lorca-relay:/data lorca-relay` |
| Success | First log: `lorca-relay listening` with `bind`, `db`, `files`, `push`; `GET /` → `Lorca Relay is running...`; `GET /v1/health` → `{"ok":true,"service":"lorca-relay","protocol":1}` |
| Shutdown | `SIGTERM` or Ctrl-C cancels sync sockets, then `db.close()` |

`temp/lorca-relay.db` is a **runtime** SQLite path created by `bun run relay` (`mkdir -p temp`). The directory `temp/` is gitignored; it is not a committed repository file. The binary default without that script is `lorca-relay.db` in the process working directory.

Docker `CMD` listens on `[::]:${PORT:-8787}` unless `LORCA_RELAY_BIND` is set. Clients point at the relay with `LORCA_RELAY_URL` (CLI) or Settings › Advanced › Relay URL.

:::files
crates/relay/
  Cargo.toml          # package lorca-relay, [[bin]] path = src/main.rs
  Dockerfile
  README.md
  src/
    main.rs           # Args, AppState, bind + graceful shutdown
    routes.rs         # HTTP/WS router + ApiError
    routes/tests.rs
    auth.rs           # signed requests, bearer tokens
    db.rs + db/       # Store trait; sqlite + postgres
    store.rs          # local dir or S3 for file ciphertext
    hub.rs            # in-process sync-socket signals
    limit.rs          # IP / identity / large-upload limits
    push.rs           # APNs + FCM
    metrics.rs        # GET /metrics
    sweep.rs          # housekeeping + identity delete
:::

## Options and defaults

Every flag maps to an env var (`lorca-relay --help`). Secret-bearing values hide env contents in help.

<ParamField body="--bind" type="SocketAddr" default="127.0.0.1:8787">
`LORCA_RELAY_BIND`. Listen address. Image default becomes `[::]:$PORT` when `PORT` is set.
</ParamField>

<ParamField body="--db" type="string" default="lorca-relay.db">
`LORCA_RELAY_DB`. SQLite path, or `postgres://` / `postgresql://` URL for shared multi-process storage.
</ParamField>

<ParamField body="--secret" type="string">
`LORCA_RELAY_SECRET`. HMAC material for bearer tokens (SHA-256 of the string). Unset → random per boot; every Device must re-auth after restart. Required when multiple processes share Postgres.
</ParamField>

<ParamField body="--quota-bytes" type="u64" default="5368709120">
`LORCA_RELAY_QUOTA_BYTES`. Ciphertext cap per identity (5 GiB). `0` disables.
</ParamField>

<ParamField body="--ip-per-minute" type="u32" default="60">
`LORCA_RELAY_IP_PER_MINUTE`. Public routes (register, auth, pairing mailbox). Burst equals the rate. `0` disables.
</ParamField>

<ParamField body="--identity-per-second" type="u32" default="50">
`LORCA_RELAY_IDENTITY_PER_SECOND`. Authenticated routes; burst is 10×. `0` disables.
</ParamField>

<ParamField body="--min-protocol" type="u32" default="0">
`LORCA_RELAY_MIN_PROTOCOL`. Clients with `Lorca-Protocol` below this get `426` on `/v1/*` except `/v1/health`. Missing header counts as protocol `0`. Relay speaks protocol `1`.
</ParamField>

<ParamField body="--inactive-days" type="u32" default="365">
`LORCA_RELAY_INACTIVE_DAYS`. Daily sweep deletes identities with no machine seen, blob write, or open socket for this many days. `0` keeps all.
</ParamField>

<ParamField body="--metrics-token" type="string">
`LORCA_RELAY_METRICS_TOKEN`. Enables `GET /metrics` for that bearer. Unset → route returns `404`.
</ParamField>

<ParamField body="--concurrent-uploads" type="usize" default="3">
`LORCA_RELAY_CONCURRENT_UPLOADS`. Concurrent bodies over 1 MiB. Wait over 30 s → `503` + `Retry-After: 5`. `0` disables.
</ParamField>

<ParamField body="--trust-proxy" type="bool" default="false">
`LORCA_RELAY_TRUST_PROXY`. Take client IP from the last `X-Forwarded-For` hop. Set only behind a proxy that overwrites that header.
</ParamField>

### File ciphertext

| Flag / env | Default / notes |
| --- | --- |
| `--files-dir` / `LORCA_RELAY_FILES_DIR` | DB path with `.files` extension (`lorca-relay.files`); conflicts with `--s3-bucket` |
| `--s3-bucket` / `LORCA_RELAY_S3_BUCKET` | Requires `--s3-endpoint` |
| `--s3-endpoint` / `LORCA_RELAY_S3_ENDPOINT` | R2 / S3 / MinIO endpoint URL |
| `--s3-region` / `LORCA_RELAY_S3_REGION` | `auto` |
| `--s3-prefix` / `LORCA_RELAY_S3_PREFIX` | empty |
| `--s3-access-key` / `LORCA_RELAY_S3_ACCESS_KEY` | Falls back to `AWS_ACCESS_KEY_ID` |
| `--s3-secret-key` / `LORCA_RELAY_S3_SECRET_KEY` | Falls back to `AWS_SECRET_ACCESS_KEY` |

With a `postgres://` DB and no `--files-dir`, the local files dir still defaults beside `lorca-relay.db` (not the URL).

### Push

| Flag / env | Notes |
| --- | --- |
| `--apns-key` / `LORCA_RELAY_APNS_KEY` | `.p8` text or file path; requires key id + team id |
| `--apns-key-id` / `LORCA_RELAY_APNS_KEY_ID` | 10-character Apple key id |
| `--apns-team-id` / `LORCA_RELAY_APNS_TEAM_ID` | Apple team id |
| `--apns-topic` / `LORCA_RELAY_APNS_TOPIC` | Bundle id; default `app.lorca` |
| `--fcm-service-account` / `LORCA_RELAY_FCM_SERVICE_ACCOUNT` | Firebase JSON text or file path |

`LORCA_RELAY_APNS_URL` and `LORCA_RELAY_FCM_URL` redirect push clients to a test server (env-only, not CLI flags).

## Constants that bound requests

| Constant | Value | Role |
| --- | --- | --- |
| `PROTOCOL` | `1` | Reported in `/v1/health`; group paging + `DELETE /v1/identity` |
| `TOKEN_TTL` | 3600 s | Bearer lifetime |
| `CHALLENGE_TTL` | 120 s | Auth challenge lifetime |
| `PAIRING_TTL` | 600 s | Pairing mailbox lifetime |
| `SIGNED_REQUEST_SKEW` | 300 s | Max clock skew on signed identity requests |
| `MAX_BLOB_BYTES` | 4 MiB | Inline blob ciphertext |
| `MAX_FILE_BLOB_BYTES` | ~100 MiB + 40 | Attachment upload body limit |
| `MAX_BODY_BYTES` | 6 MiB | Default JSON body limit |
| `MAX_PUSH_BYTES` | 2560 | Push ciphertext |
| `LARGE_UPLOAD` | 1 MiB | Semaphore threshold |
| `PING_SECONDS` | 25 | Sync WebSocket ping interval |

Blob kinds: `roster`, `chat`, `job`, `job_cancel`, `job_result`, `request`, `response`, `machine`, `credentials`, `key`, `file`. Kind `file` must use `/v1/files/{id}`.

## HTTP surface

| Method | Path | Auth |
| --- | --- | --- |
| `GET` | `/` | none |
| `GET` | `/v1/health` | none |
| `GET` | `/metrics` | bearer = `--metrics-token` |
| `POST` | `/v1/identities` | signed identity payload |
| `POST` | `/v1/auth/challenge` | none (IP-limited) |
| `POST` | `/v1/auth/verify` | machine signature over nonce |
| `GET` | `/v1/sync` | bearer → WebSocket |
| `DELETE` | `/v1/identity` | bearer |
| `GET` / `DELETE` | `/v1/machines`, `/v1/machines/{machine_pubkey}` | bearer |
| `GET` / `PUT` | `/v1/blobs` | bearer |
| `GET` / `DELETE` | `/v1/blobs/{id}` | bearer |
| `GET` / `PUT` | `/v1/files/{id}` | bearer; PUT needs `application/octet-stream` |
| `DELETE` | `/v1/groups/{group}` | bearer |
| `GET` | `/v1/groups/{group}/blobs` | bearer |
| `POST` | `/v1/push` | bearer |
| `PUT` / `DELETE` | `/v1/push/token` | bearer |
| `POST` / `DELETE` | `/v1/pair`, `/v1/pair/{nonce}` | bearer (owner) |
| `POST` / `GET` | `/v1/pair/{nonce}/request` | join posts without auth; owner GETs |
| `POST` / `GET` | `/v1/pair/{nonce}/reply` | owner posts; join GETs without auth |

<RequestExample>
```bash
curl -s http://127.0.0.1:8787/v1/health
```
</RequestExample>

<ResponseExample>
```json
{"ok":true,"service":"lorca-relay","protocol":1}
```
</ResponseExample>

<RequestExample>
```http
PUT /v1/files/att-1?group=chat-abc HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/octet-stream

<binary ciphertext>
```
</RequestExample>

<ResponseExample>
```json
{"id":"att-1","seq":42,"existing":false}
```
</ResponseExample>

## Error responses

JSON errors use `{ "error": "<message>" }` unless noted. Rate limits add `Retry-After`.

| Status | When |
| --- | --- |
| `400` | Bad base64url/key/signature shape, unknown kind, invalid id/slot/group, empty/oversized ciphertext, wrong blob route for `file`, bad push platform/token |
| `401` | Missing/malformed/expired bearer, bad signature, unknown/expired challenge, signed request too old |
| `403` | Pairing not owned by caller (`Not your pairing`) |
| `404` | Unknown machine/blob/file; metrics when token unset; missing file object (`The file is no longer stored`); unknown/expired pairing |
| `409` | Identity content key mismatch; group already deleted; pairing already has a request |
| `410` | Machine unpaired / revoked |
| `413` | Storage quota exceeded |
| `415` | File upload without `application/octet-stream` |
| `426` | Client protocol below `--min-protocol` — body includes `min_protocol` and `protocol` |
| `429` | IP or identity rate limit (`Too many requests` + `Retry-After`) |
| `503` | Large-upload semaphore wait timed out (`Too many uploads at once; try again`) |
| `500` | Database / internal failures (`Database error`) |
| `204` | Successful deletes / push-token updates / pairing posts with no body |

Startup failures (missing S3 keys, unreadable APNs/FCM key path, bind/DB open errors) exit the process via `anyhow` before serving.

Postgres with a self-signed peer and web PKI roots fails TLS unless the URL includes `sslmode=disable` (common on Railway private networking).

## Storage and multi-process

```text
Client ──HTTP/WS──► lorca-relay (axum)
                      │
                      ├─ db::Store ──► SQLite file  OR  Postgres (+ LISTEN/NOTIFY)
                      ├─ FileStore ──► local .files dir  OR  S3/R2/MinIO
                      ├─ Hub (this process) ──► sync socket signals
                      └─ Pusher ──► APNs / FCM (optional)
```

- **SQLite**: one process owns the file.
- **Postgres**: many processes share DB + must share `--secret` and an S3 bucket for files. Without `--secret`, tokens from one process fail on another. Without S3, other hosts cannot read attachment objects.
- Housekeeping: `Store::tick` every 60 s; `sweep` hourly after a 10-minute delay (stale sealed envelopes, deleted-group marks) and daily (inactive identities, usage recount, orphan file objects older than 1 day).

## Related pages

<CardGroup>
  <Card title="HTTP API reference" href="/http-api-reference">
    Route-level request and response shapes across the product surface.
  </Card>
  <Card title="Architecture" href="/architecture">
    Identity, pairing, and zero-knowledge relay protocol.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Env vars and defaults across CLI, relay, and clients.
  </Card>
  <Card title="Deployment and operations" href="/deployment">
    Docker, Railway, Postgres, and S3 runbooks.
  </Card>
  <Card title="crates/cli reference" href="/ref-crates-cli">
    Client that talks to this relay (`LORCA_RELAY_URL`, sync session).
  </Card>
</CardGroup>

Next: run `cargo run -q -p lorca-relay -- --bind 127.0.0.1:8787 --db lorca-relay.db` and `curl -s http://127.0.0.1:8787/v1/health`.
