---
name: ricochet-server
description: Use when building, configuring, deploying or operating the go-ricochet store-and-forward server (module github.com/stephanfeb/go-ricochet, cmd/ricochet): config.yaml keys, presets, PostgreSQL schema, transports, relay/NAT, the .deb and supervisor package, the ops HTTP surface, moderation, and its protocol IDs.
triggers: go-ricochet, ricochet server, cmd/ricochet, ricochet-server, config.example.yaml, RICOCHET_PG_PASSWORD, RICOCHET_SEED_HEX, /sf-network/submit, /ricochet/store/doc, ricochet-bench, max_messages_per_mailbox, enable_relay_service
---

# ricochet-server

## What it is

go-ricochet is the Go server of the Ricochet protocol: a libp2p peer that
stores messages for peers that are offline (an MX-style mail server for P2P),
plus document, feed, collection and directory stores, all in PostgreSQL. It
listens on UDX by default, authenticates every peer by its Noise handshake,
and pushes notifications to connected clients.

Use this skill to run or change the server. For client code use
`ricochet-dart-client` (Dart/Flutter) or `ricochet-mailbox` (the Go client
`pkg/client` and the end-to-end mailbox pattern). For the framework the
protocol handlers are built on, see `go-p2p-forge`.

## Install

Current release: **v1.6.0** (Go 1.26.10+, PostgreSQL 14+).

```bash
git clone https://github.com/stephanfeb/go-ricochet && cd go-ricochet
go build -o ricochet ./cmd/ricochet
go build -o ricochet-bench ./cmd/ricochet-bench
```

Debian/Ubuntu package (release asset `ricochet-server_X.Y.Z_{amd64,arm64}.deb`,
or build it):

```bash
./build-deb.sh                                   # on macOS, inside Docker (Dockerfile.build)
VERSION=1.6.0 ARCH=amd64 scripts/package-deb.sh  # directly on Linux
sudo apt install ./build/dist/ricochet-server_1.6.0_amd64.deb
```

## Core workflow

### 1. Database

The schema is idempotent (`CREATE TABLE IF NOT EXISTS`): run it on install and
again after every upgrade.

```bash
psql postgres -c "CREATE ROLE ricochet LOGIN PASSWORD 'secret'"
createdb ricochet
psql ricochet < schema.sql
```

### 2. Run

```bash
export RICOCHET_PG_PASSWORD=secret
./ricochet --development --pg-host localhost --pg-database ricochet \
  --pg-username ricochet --pg-sslmode disable

./ricochet --production --data-dir /var/lib/ricochet \
  --pg-host db.example.com --pg-database ricochet --pg-username ricochet \
  --external-ip 203.0.113.10
```

It prints `Ricochet server <version> running. Peer ID: 12D3KooW...`. The
address clients dial is `/ip4/<public-ip>/udp/55223/udx/p2p/<peer-id>`.

CLI flags: `--port` (55223), `--development`, `--production`,
`--high-capacity`, `--config`, `--data-dir`, `--identity-file`,
`--transports`, `--listen-addrs`, `--external-ip`, `--external-addrs`,
`--debug-dht`, `--pg-host`, `--pg-port`, `--pg-database`, `--pg-username`,
`--pg-password`, `--pg-sslmode` (`require` default, `disable`), `--version`.

Precedence: preset defaults, then config file (`--config`, or
`/etc/ricochet/config.yaml` when it exists), then CLI flags.

### 3. Config file (keys verified in `internal/core/config.go`)

```yaml
server:
  port: 55223
  region: eu-west
  transports: [udx]                # udx, tcp, quic-v1, websocket, webtransport
  listen_addresses: [/ip4/0.0.0.0/udp/55223/udx]
  external_ip: 203.0.113.10        # advertises every listen address on this IP
  bootstrap_peers: []
storage:
  data_directory: /var/lib/ricochet/sf_storage
  max_storage_gb: 50
  retention_days: 30
  max_messages_per_mailbox: 1000
  max_mailboxes: 100000
  max_mailboxes_per_owner: 100
  max_entries_per_feed: 10000
  max_document_history_versions: 10
database: {host: localhost, port: 5432, name: ricochet, username: ricochet, sslmode: require, pool_size: 25}
performance: {max_concurrent_connections: 10000, max_connections_per_ip: 0, connection_timeout_sec: 30, message_timeout_sec: 10, worker_threads: 4}
features:
  enable_authentication: false     # only security.trusted_peers may connect
  enable_forwarding: false
  enable_relay: true
  enable_relay_service: true
  enable_auto_relay: false
  enable_hole_punching: false
security:
  trusted_peers: []
  moderators: []
audit: {connection_log_days: 90}
pubsub: {relay_topics: []}
ops: {enabled: true, bind: 127.0.0.1, port: 9090}
admission_control: {enabled: true, max_in_flight: 0, max_in_flight_per_peer: 64, acquire_timeout: 5s}
```

`config.example.yaml` documents every key, including `relay_limits`,
`rate_limiting`, `intervals` and the rest of `ops`; copy from it rather than
writing keys from memory.

### 4. Package install (supervisor)

The .deb installs `/opt/ricochet/{ricochet_server,run.sh,schema.sql,health_check.sh}`,
`/etc/ricochet/{config.yaml,config.yaml.example,env.example}` and
`/etc/supervisor/conf.d/ricochet.conf`, creates the `ricochet` user, and
copies `env.example` to `/etc/ricochet/env` (mode 640) on first install.

```bash
sudoedit /etc/ricochet/env        # DB_PASSWORD=..., EXTERNAL_IP=203.0.113.10
psql -U ricochet -d ricochet -f /opt/ricochet/schema.sql
sudo supervisorctl start ricochet
sudo supervisorctl tail -f ricochet
/opt/ricochet/health_check.sh     # Nagios codes: 0 ok, 1 not ready, 2 down
```

`run.sh` sources `/etc/ricochet/env`, exports `DB_PASSWORD` as
`RICOCHET_PG_PASSWORD`, and runs `--production` with `DB_HOST`, `DB_PORT`,
`DB_NAME`, `DB_USER`, `DB_SSLMODE`, `LISTEN_PORT` and `EXTERNAL_IP`. A first
install is left stopped; an upgrade restarts a server that was running.

### 5. Docker

There is no published server image. `Dockerfile.build` is only the .deb
build environment. For a containerised server, copy
`examples/mailbox/docker/server.Dockerfile` (multi-stage, `CGO_ENABLED=0`,
`EXPOSE 55223/udp`) and its entrypoint, which passes `--data-dir /data`
(a volume, so the peer ID survives) and `--external-addrs`.

### 6. Operate

```bash
curl -s 127.0.0.1:9090/healthz            # liveness, never touches the DB
curl -s 127.0.0.1:9090/readyz             # 503 when PostgreSQL is down or draining
curl -s 127.0.0.1:9090/metrics            # Prometheus, needs features.enable_metrics
curl -s '127.0.0.1:9090/ops/mailboxes/top?n=20'
curl -s '127.0.0.1:9090/ops/mailboxes?owner=12D3KooW...'
curl -s 127.0.0.1:9090/ops/storage
curl -s 127.0.0.1:9090/ops/limits         # the limits the server actually parsed
ssh -L 9090:127.0.0.1:9090 host           # the ops listener is loopback-only
```

Load test: `ricochet-bench -n 1000 -c 10 -protocol msa /ip4/127.0.0.1/udp/55223/udx 12D3KooW...`
(protocols `msa`, `maa`, `sda`, `sfa`, `sca`, `mma`, `mixed`, `sda-batch`,
`msa-batch`, `sync-cold`, `sync-warm`, `sync-noop`).

## Key concepts / API map

| Agent | Protocol ID | Role |
|---|---|---|
| MSA | `/sf-network/submit/1.0.0` | Message submission |
| MSA batch | `/sf-network/submit/batch/1.0.0` | Many submissions per request |
| MAA | `/sf-network/access/1.0.0` | Retrieve, flags, expunge, delete, claim |
| MMA | `/sf-network/admin/1.0.0` | Mailbox create/delete, ACLs, capacity |
| SDA | `/ricochet/store/doc/1.0.0` | Documents and the directory |
| SFA | `/ricochet/store/feed/1.0.0` | Append-only feeds |
| SCA | `/ricochet/store/collection/1.0.0` | Keyed collections with queries |
| MOA | `/ricochet/moderation/1.0.0` | `ban`, `unban`, `getBan`, `listBans`, `removePost`, `connections` |
| Notify | `/ricochet/mailbox-notify/1.0.0` | Server-to-client push for private mailboxes |
| MTA, MDA | internal | Routing/validation; storage and push |

GossipSub topics: `/sf-network/services/announce` (every
`service_announcement_min`), `/sf-network/services/heartbeat` (every
`health_check_min`), `/sf-network/presence/<serverPeerID>`, and
`/ricochet/mailbox/<owner>/<folder>` for shared and public mailbox pushes.

Wire format: every frame is a 4-byte big-endian length then JSON, max 10 MB.

Mailbox types: `private` (anyone may deposit for the owner, only the owner
reads), `shared` (ACL), `public` (anyone reads, ACL writes), `claim` (v1.6.0:
owner stores, anyone claims one message at a time, 30 claims an hour per
mailbox).

Presets: default and `--production` 10,000 connections; `--development` 1 GB
and 100 connections, TLS to PostgreSQL off; `--production` 50 GB, pool 50;
`--high-capacity` 100 GB, 50,000 connections, forwarding on, 8 push workers.

Identity: `RICOCHET_SEED_HEX`, then `--identity-file` / `server.identity_file`
(key file, or a 32-byte seed as hex or base64), then
`{data-dir}/peer_identity.key`, generated on first start.

## Gotchas

- **An unknown config key stops the server at startup.** A `logging:` block
  (common in pre-v1.0.0 configs) fails with `field logging not found`; delete
  it. Log level is INFO, DEBUG under `--development`; library logs follow
  `GOLOG_LOG_LEVEL` and `--debug-dht`.
- **The rate-limit block is `rate_limiting:`**, not `rate_limits:` as the
  README prose says. Limits are off by design: throughput is governed by
  `admission_control`, and a shed request is a 503, a signal to add capacity.
- **`relay_limits` keys carry their units**: `reservation_ttl_min`,
  `connection_duration_sec`. `reservation_ttl` or `connection_duration` are
  silently ignored there.
- **Set the public address.** Without `external_ip` / `--external-ip` /
  `EXTERNAL_IP`, Identify advertises only private IPs and clients lose the
  server's address after the first connection drop.
- **`--port` moves every listen address to one port.** UDX and
  `quic-v1`/`webtransport` each need their own UDP socket, so a server running
  both sets ports in `listen_addresses` and leaves `--port`/`LISTEN_PORT`
  unset. Every listen address needs its transport in `server.transports`.
  `webrtc-direct` is refused.
- **On Linux, binding `[::]` and `0.0.0.0` on the same port fails**: the IPv6
  socket is dual-stack.
- **The peer ID is the address.** Losing `peer_identity.key` (or the data
  volume in Docker) changes the ID every client has pinned. Back it up.
- **`--production` requires TLS to PostgreSQL.** Add `--pg-sslmode disable`
  only for a database on the same host or a trusted private network.
- **Never pass `--pg-password` in production**: argv is readable by every
  local user. Use `RICOCHET_PG_PASSWORD` or `DB_PASSWORD` in `/etc/ricochet/env`.
- **`enable_authentication: true` with an empty `trusted_peers` is a startup
  error.** Authentication is an allow-list; no preset turns it on.
- **Identity is free, so per-peer limits are fairness, not abuse control.**
  `max_connections_per_ip` is the only limit a client cannot rotate out of, and
  it is 0 (off) on purpose: carrier NAT puts many phones on one address.
- **Mailbox defaults apply at creation only.** Changing
  `max_messages_per_mailbox` or `retention_days` affects new mailboxes; an
  existing one keeps what it was created with. A full mailbox rejects
  deliveries with 507 until the owner drains it: watch
  `ricochet_mailboxes_near_capacity` and `/ops/mailboxes/top`.
- **`retention_days: 0` means "use the default"**, never "keep nothing"; a
  value under a day is rounded up to one.
- **`max_storage_gb` is enforced** from the periodic capacity sample (every
  `cleanup_min`), so a burst can overrun it by one interval's writes.
- **Delivery auto-creates a missing folder as a private (open) mailbox**, and
  the type is fixed for life. `max_mailboxes_per_owner` (100) is what stops a
  sender creating unbounded folders in someone else's namespace.
- **The relay does not cut relayed connections** unless you set
  `relay_limits.connection_duration_sec` or `connection_data`. go-libp2p's own
  defaults (120 s, 128 KiB) only cover a hole punch. Hole punching between
  clients needs no server setting; `enable_hole_punching` is for the server's
  own relayed connections behind a NAT, with `enable_auto_relay` and relays in
  `bootstrap_peers`.
- **GossipSub forwards only topics a peer announces.** If apps meet only
  through this server on a topic it does not use, list it in
  `pubsub.relay_topics` (for example `/overmedia/service-announce`).
- **Stored documents, feeds and collections are plaintext in PostgreSQL.**
  Visibility is enforced by the server, not a key. Encrypt the volume, and
  tell clients to encrypt anything the operator must not read.
- **Message E2E encryption hides only the payload.** Sender, recipient,
  folder, timestamps, size, priority and flags are visible to the server, and
  there is no forward secrecy.
- **Push notifications are best effort.** Past `worker_threads` in flight the
  surplus is dropped; clients must poll as well.
- **Moderation**: a banned peer may still connect but only reaches its
  `trust-safety` folder; nothing is deleted, `unban` restores it all. The
  connection log (`peer_connections`) holds IPs for `connection_log_days`.
  Run `schema.sql` after upgrading or `peer_bans` / `peer_connections` are
  missing.
- **Shutdown**: `/readyz` goes 503, the server keeps serving for
  `ops.drain_delay`, then waits `ops.shutdown_timeout` for requests in flight.
  Behind a load balancer set `drain_delay` to a couple of probe intervals.
  supervisor's `stopwaitsecs=30` must exceed both.
- **Go toolchain**: go.mod requires 1.26.10 (GO-2026-6809). Two govulncheck
  advisories (GO-2026-4479, GO-2024-3218) are known and unreachable.

## Testing

```bash
go test $(go list ./... | grep -v test/integration)   # unit, no database
RICOCHET_TEST_POSTGRES_DSN="postgresql://user:pass@localhost:5432/ricochet_test?sslmode=disable" \
  go test ./test/integration/...                      # full client-server over UDX
(cd examples/mailbox/docker && ./demo.sh)             # server + two clients behind NAT
test/nat/run.sh                                        # hole punch through the relay
```

Handler tests use the in-process harness `internal/protocol/protocoltest`
and the in-memory store `internal/storage/storagetest`. Run the integration
suite on a fresh database after each change. Issues are tracked with `bd`
(beads), not `doc/BACKLOG.md`.

## Red lines

- Never change a protocol ID, the frame format or a JSON field name without a
  new protocol version: the Go, Dart and JS clients all depend on them.
- Never trust a peer ID claimed in a request body; authorise against the
  connection's authenticated peer.
- Never bind `ops` off loopback without an authenticating proxy: `/ops/*`
  names peers and their usage, and pprof exposes heap contents.
- Never commit `/etc/ricochet/env`, a seed or `peer_identity.key`.
- Never delete and recreate a user's mailbox to change its type; it drops
  undelivered messages.
- Never set `sslmode: disable` across an untrusted network.

## Reference

- Server docs: https://cryptopeer.net/ricochet/server/
- Overview: https://cryptopeer.net/ricochet/
- Mailbox example: https://cryptopeer.net/ricochet/mailbox/
- Repository: https://github.com/stephanfeb/go-ricochet (`README.md`,
  `config.example.yaml`, `doc/MAILBOX_LIFECYCLE_AND_ACLS.md`,
  `doc/SUPERVISORD_DEPLOYMENT.md`, `doc/RELEASING.md`)
- Related skills: `ricochet-dart-client`, `ricochet-mailbox`, `go-p2p-forge`,
  `udx-go`
