Skip to content

ricochet-server

Skill ricochet-server. 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.

SKILL.md (Claude Code, Codex, Cursor) · ricochet-server.md (Boubou) · install: curl -fsSL https://cryptopeer.net/skills/install.sh | sh -s -- ricochet-server

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.

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

Terminal window
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):

Terminal window
./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

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

Terminal window
psql postgres -c "CREATE ROLE ricochet LOGIN PASSWORD 'secret'"
createdb ricochet
psql ricochet < schema.sql
Terminal window
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)

Section titled “3. Config file (keys verified in internal/core/config.go)”
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.

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.

Terminal window
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.

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.

Terminal window
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).

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.

  • 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.
Terminal window
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.

  • 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.

Built by Stephan Februarygithub.com/stephanfebLinkedIn