A store-and-forward server for libp2p
The mail server, rebuilt for peer-to-peer.
Peers go offline. go-ricochet holds their messages, documents and feeds until they come back, the way SMTP and NNTP servers did for desktop software before platforms absorbed the client, the server and the data. One node is also a libp2p relay, a GossipSub host and a DHT server.
go get github.com/stephanfeb/go-ricochet/pkg/clientThe idea is old
We had this once.
Store-and-forward made the desktop internet work: you chose your software, someone nearby ran a server, and open protocols connected them all.
1990s · Store and forward
Your client, anyone's server
Mail and news clients on the desktop. SMTP, NNTP and IMAP servers run by ISPs, universities and hobbyists. The protocol was the product.
2010s · The platform
One company's app
Client, server and data merged into a single service. Interoperation stopped. Your contacts, history and audience live in someone else's database.
Now · Ricochet
Store-and-forward, upgraded
Identity is a keypair, not an account. Payloads are end-to-end encrypted. Servers are interchangeable, listed by priority like MX records. NAT traversal is built in.
Who it's for
Three kinds of people run into the offline-peer problem.
App developers
You are writing a desktop or mobile app on libp2p, and your users are not online at the same time. You want mailboxes, sync and push without building a backend.
Developer guide →Operators
You run a node for a community, a team or yourself, the way people once ran a mail or news server. One binary, PostgreSQL, health checks and Prometheus metrics.
User guide →Network builders
You need public infrastructure for a libp2p network: a Circuit Relay v2 service, a DHT server, a bootstrap peer that relays your GossipSub topics.
Networking chapter →How it works
Built like a mail server, one agent per job.
Each agent is a libp2p protocol. If you know SMTP and IMAP, you already know the shape.
Alice's client opens a stream to the server Bob prefers and hands over the message. Encrypted payloads stay sealed: only Bob's key opens them.
cl.SendMessage(ctx, bobID, payload, client.WithEncryption())The transfer agent validates and routes it; the delivery agent writes it to PostgreSQL in the right mailbox and folder.
// server side: MTA → MDA → PostgreSQLIf Bob is connected, he gets a push straight away: a direct stream for a private mailbox, GossipSub for shared and public ones.
cl.RegisterNotificationHandler(func(n *wire.Notification) { … })Whenever Bob comes online, his client retrieves what is waiting and acknowledges it. Nothing is removed until he does.
cl.RetrieveMessages(ctx) → cl.MarkDelivered(ctx, ids)One node, many roles
Everything a P2P app needs from infrastructure.
Mailboxes
Private, shared and public. ACLs, IMAP-style flags, expunge, retention.
Document store
Key-value documents with ETags, conditional writes and merge-patch.
Feeds & collections
Append-only feeds, collaborative feeds, keyed collections with queries.
Directory
A searchable listing that each peer manages for itself.
Push notifications
Direct streams for private mailboxes, GossipSub for shared and public.
End-to-end encryption
NaCl box keyed from the peers' Ed25519 identities. The server stores ciphertext.
Relay & NAT traversal
Circuit Relay v2 on by default. AutoRelay and DCUtR hole punching when you need them.
GossipSub & DHT host
Kademlia in server mode, relayed topics, signed service announcements.
What it's for
Applications that suit store-and-forward.
Offline-first messaging
Chat and mail between peers that are rarely online at the same time.
Desktop sync
Notes vaults, settings and drafts kept in step across your own machines.
Boards & newsgroups
NNTP in spirit: public mailboxes and collaborative feeds anyone can read.
Team workspaces
Shared mailboxes and collections with per-peer read and write grants.
Profiles & discovery
Public profile documents and a directory peers can browse and search.
Network bootstrap
Relay, DHT and topic relaying for an app network with no servers of its own.
What it isn't
Know the trade-offs before you build.
Store-and-forward has costs. Better to hear them here than to find them in production.
Not anonymous
The server sees the envelope: sender, recipient, folder and timestamps.
No forward secrecy
By design. If you need it, run a ratchet such as Signal on top.
Limits are fairness, not security
Identities cost nothing to make, so per-peer quotas share capacity. They don't stop a determined abuser.
Not a platform
No accounts, no global index, no ranking. Your app decides what people see.
For developers
A client in a few lines of Go.
Bring your own go-libp2p host with the UDX transport. The client tries your servers in priority order and reports every refusal as a typed error.
// The server speaks UDX only: give the host that transport.
h, _ := libp2p.New(
libp2p.NoTransports,
libp2p.Transport(udxtransport.NewTransport),
libp2p.ListenAddrStrings("/ip4/0.0.0.0/udp/0/udx"),
libp2p.Security(noise.ID, noise.New),
libp2p.Muxer("/yamux/1.0.0", yamux.DefaultTransport),
)
cl := client.New(h, client.Config{
PreferredServers: []client.ServerPreference{
{PeerID: serverPeerID, Priority: 1},
},
})
defer cl.Close()
// Bob is offline. The server holds this until he collects it.
result, err := cl.SendMessage(ctx, bobID, []byte("hello"),
client.WithEncryption(),
client.WithExpiry(24 * time.Hour),
)// Hours later, Bob comes online and collects his mail.
messages, err := cl.RetrieveMessages(ctx,
client.WithMaxMessages(50),
)
ids := make([]string, 0, len(messages))
for _, m := range messages {
ids = append(ids, m.MessageID)
}
// Retrieval never removes anything on its own: acknowledge.
ack, err := cl.MarkDelivered(ctx, ids)ownerID := cl.PeerID()
putResp, err := cl.PutDocument(ctx, ownerID, "config/settings",
[]byte(`{"theme":"dark"}`),
client.WithContentType("application/json"),
)
// Only succeeds if no other device changed it since.
_, err = cl.PutDocument(ctx, ownerID, "config/settings",
[]byte(`{"theme":"light"}`),
client.WithIfMatch(putResp.ETag),
)
if errors.Is(err, client.ErrConflict) {
// fetch, merge, retry
}cl.CreateMailbox(ctx, "team-updates", wire.MailboxShared)
cl.GrantAccess(ctx, "team-updates", colleaguePeerID, wire.AccessReadWrite)
cl.CreateMailbox(ctx, "announcements", wire.MailboxPublic,
client.WithMailboxMaxMessages(500),
client.WithRetentionDays(7),
)
cl.RegisterNotificationHandler(func(n *wire.Notification) {
fmt.Printf("New message in %s (type: %s)\n",
n.MailboxPath, n.MailboxType)
})For operators
There is no hosted service. Run your own node.
Every server is somebody's own. Start one on a laptop to develop against, then put it on a public host for your users. You need PostgreSQL 14 or later.
curl -LO https://github.com/stephanfeb/go-ricochet/releases/download/v1.1.0/ricochet_1.1.0_linux_amd64.tar.gz
tar xzf ricochet_1.1.0_linux_amd64.tar.gz
cd ricochet_1.1.0_linux_amd64
psql postgres -c "CREATE ROLE ricochet LOGIN PASSWORD 'secret'"
createdb ricochet && psql ricochet < schema.sql
export RICOCHET_PG_PASSWORD=secret
./ricochet --development --pg-host localhost \
--pg-database ricochet --pg-username ricochet --pg-sslmode disablecurl -LO https://github.com/stephanfeb/go-ricochet/releases/download/v1.1.0/ricochet_1.1.0_darwin_arm64.tar.gz
tar xzf ricochet_1.1.0_darwin_arm64.tar.gz
cd ricochet_1.1.0_darwin_arm64
psql postgres -c "CREATE ROLE ricochet LOGIN PASSWORD 'secret'"
createdb ricochet && psql ricochet < schema.sql
export RICOCHET_PG_PASSWORD=secret
./ricochet --development --pg-host localhost \
--pg-database ricochet --pg-username ricochet --pg-sslmode disablecurl -LO https://github.com/stephanfeb/go-ricochet/releases/download/v1.1.0/ricochet-server_1.1.0_amd64.deb
sudo apt install ./ricochet-server_1.1.0_amd64.deb
# Load /opt/ricochet/schema.sql into your database, then set
# DB_PASSWORD and EXTERNAL_IP in /etc/ricochet/env
sudo supervisorctl start ricochetgit clone https://github.com/stephanfeb/go-ricochet
cd go-ricochet
go build -o ricochet ./cmd/ricochet
./ricochet --versionPresets for development, production and high capacity. Health checks, readiness and Prometheus metrics on a separate ops listener.
Clients & ecosystem
Reach it from Go, Dart or JavaScript.
Ricochet is a libp2p server that listens on UDX, a reliable transport over UDP. Any libp2p host with a UDX transport can connect: go-udx, dart-udx and js-udx provide one.
Dart client
For Flutter and Dart apps on dart_libp2p 3.
UDX transport
go-udx, dart-udx and js-udx.
Your own client
Protocol IDs, framing and status codes for any language.
go-p2p-forge
The server framework ricochet is built on. Fork and add your own protocols.
cryptopeer.net
The wider family of libp2p libraries ricochet is built with.