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
What it is
Section titled “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
Section titled “Install”Current release: v1.6.0 (Go 1.26.10+, PostgreSQL 14+).
git clone https://github.com/stephanfeb/go-ricochet && cd go-ricochetgo build -o ricochet ./cmd/ricochetgo build -o ricochet-bench ./cmd/ricochet-benchDebian/Ubuntu package (release asset ricochet-server_X.Y.Z_{amd64,arm64}.deb,
or build it):
./build-deb.sh # on macOS, inside Docker (Dockerfile.build)VERSION=1.6.0 ARCH=amd64 scripts/package-deb.sh # directly on Linuxsudo apt install ./build/dist/ricochet-server_1.6.0_amd64.debCore workflow
Section titled “Core workflow”1. Database
Section titled “1. Database”The schema is idempotent (CREATE TABLE IF NOT EXISTS): run it on install and
again after every upgrade.
psql postgres -c "CREATE ROLE ricochet LOGIN PASSWORD 'secret'"createdb ricochetpsql ricochet < schema.sql2. Run
Section titled “2. Run”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.10It 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: 10database: {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: falsesecurity: 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)
Section titled “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.
sudoedit /etc/ricochet/env # DB_PASSWORD=..., EXTERNAL_IP=203.0.113.10psql -U ricochet -d ricochet -f /opt/ricochet/schema.sqlsudo supervisorctl start ricochetsudo supervisorctl tail -f ricochet/opt/ricochet/health_check.sh # Nagios codes: 0 ok, 1 not ready, 2 downrun.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
Section titled “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
Section titled “6. Operate”curl -s 127.0.0.1:9090/healthz # liveness, never touches the DBcurl -s 127.0.0.1:9090/readyz # 503 when PostgreSQL is down or drainingcurl -s 127.0.0.1:9090/metrics # Prometheus, needs features.enable_metricscurl -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/storagecurl -s 127.0.0.1:9090/ops/limits # the limits the server actually parsedssh -L 9090:127.0.0.1:9090 host # the ops listener is loopback-onlyLoad 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
Section titled “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
Section titled “Gotchas”- An unknown config key stops the server at startup. A
logging:block (common in pre-v1.0.0 configs) fails withfield logging not found; delete it. Log level is INFO, DEBUG under--development; library logs followGOLOG_LOG_LEVELand--debug-dht. - The rate-limit block is
rate_limiting:, notrate_limits:as the README prose says. Limits are off by design: throughput is governed byadmission_control, and a shed request is a 503, a signal to add capacity. relay_limitskeys carry their units:reservation_ttl_min,connection_duration_sec.reservation_ttlorconnection_durationare 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. --portmoves every listen address to one port. UDX andquic-v1/webtransporteach need their own UDP socket, so a server running both sets ports inlisten_addressesand leaves--port/LISTEN_PORTunset. Every listen address needs its transport inserver.transports.webrtc-directis refused.- On Linux, binding
[::]and0.0.0.0on 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. --productionrequires TLS to PostgreSQL. Add--pg-sslmode disableonly for a database on the same host or a trusted private network.- Never pass
--pg-passwordin production: argv is readable by every local user. UseRICOCHET_PG_PASSWORDorDB_PASSWORDin/etc/ricochet/env. enable_authentication: truewith an emptytrusted_peersis 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_ipis 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_mailboxorretention_daysaffects new mailboxes; an existing one keeps what it was created with. A full mailbox rejects deliveries with 507 until the owner drains it: watchricochet_mailboxes_near_capacityand/ops/mailboxes/top. retention_days: 0means “use the default”, never “keep nothing”; a value under a day is rounded up to one.max_storage_gbis enforced from the periodic capacity sample (everycleanup_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_secorconnection_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_punchingis for the server’s own relayed connections behind a NAT, withenable_auto_relayand relays inbootstrap_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_threadsin flight the surplus is dropped; clients must poll as well. - Moderation: a banned peer may still connect but only reaches its
trust-safetyfolder; nothing is deleted,unbanrestores it all. The connection log (peer_connections) holds IPs forconnection_log_days. Runschema.sqlafter upgrading orpeer_bans/peer_connectionsare missing. - Shutdown:
/readyzgoes 503, the server keeps serving forops.drain_delay, then waitsops.shutdown_timeoutfor requests in flight. Behind a load balancer setdrain_delayto a couple of probe intervals. supervisor’sstopwaitsecs=30must 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
Section titled “Testing”go test $(go list ./... | grep -v test/integration) # unit, no databaseRICOCHET_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 NATtest/nat/run.sh # hole punch through the relayHandler 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
Section titled “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
opsoff loopback without an authenticating proxy:/ops/*names peers and their usage, and pprof exposes heap contents. - Never commit
/etc/ricochet/env, a seed orpeer_identity.key. - Never delete and recreate a user’s mailbox to change its type; it drops undelivered messages.
- Never set
sslmode: disableacross an untrusted network.
Reference
Section titled “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