Push notifications
This page shows how a connected client learns that new mail has arrived without polling in a tight loop. A notification says “something arrived in this mailbox”; it does not carry the message. You retrieve the message as usual.
How the server notifies
Section titled “How the server notifies”When the server stores a message, it sends a notification according to the mailbox type:
| Mailbox type | Channel | Who receives it |
|---|---|---|
| Private | A direct stream on /ricochet/mailbox-notify/1.0.0 to the mailbox owner |
The owner, if connected to this server |
| Shared, public | A GossipSub message on the topic /ricochet/mailbox/<owner peer ID>/<folder path> |
Every peer subscribed to the topic through this server |
Both carry the same JSON payload, decoded as a *wire.Notification:
| Field | Meaning |
|---|---|
MailboxPath |
The folder, such as inbox. |
MailboxType |
private, shared or public. |
MessageCount |
Messages this notification is for (1 per delivery). |
Timestamp |
Unix seconds. |
ServerPeerID |
The server that stored the message: retrieve from it. |
Before opening a direct stream, the server checks that the owner is connected, and skips the notification if not. A client behind NAT receives notifications over the connection it opened, so it does not need to be reachable.
Private mailboxes: RegisterNotificationHandler
Section titled “Private mailboxes: RegisterNotificationHandler”cl.RegisterNotificationHandler(func(n *wire.Notification) { log.Printf("new mail in %s on %s", n.MailboxPath, n.ServerPeerID)})The handler runs on a new goroutine for each notification, so it must be safe to call concurrently.
Keep it short: signal your retrieve loop rather than retrieving inside the handler. cl.Close() removes
the handler from the host.
This pattern collapses a burst of notifications into one retrieve:
newMail := make(chan struct{}, 1)cl.RegisterNotificationHandler(func(n *wire.Notification) { select { case newMail <- struct{}{}: default: // a retrieve is already pending }})
poll := time.NewTicker(time.Minute)defer poll.Stop()for { select { case <-ctx.Done(): return ctx.Err() case <-newMail: case <-poll.C: } drainInbox(ctx, cl) // your function: RetrieveMessages, handle, MarkDelivered}Shared and public mailboxes: subscribe to the topic
Section titled “Shared and public mailboxes: subscribe to the topic”pkg/client does not subscribe to GossipSub for you. Run a GossipSub router on the same host, connected
to the server, and subscribe to the mailbox’s topic:
import pubsub "github.com/libp2p/go-libp2p-pubsub"
ps, err := pubsub.NewGossipSub(ctx, h) // h: the host you passed to client.Newif err != nil { return err}topic, err := ps.Join("/ricochet/mailbox/" + ownerID.String() + "/announcements")if err != nil { return err}sub, err := topic.Subscribe()if err != nil { return err}for { msg, err := sub.Next(ctx) if err != nil { return err } n, err := wire.DecodeNotification(msg.Data) if err != nil { continue } signalRetrieve(n) // then RetrieveMessages with WithTargetPeer(ownerID)}The server joins a mailbox’s topic when it first publishes on it and leaves after 15 minutes without a notification. Subscribers keep their own subscription regardless; GossipSub needs a few heartbeats after you join before messages flow, so subscribe early. Anyone who knows the owner and folder can subscribe to the topic, so a notification for a shared mailbox reveals that it received mail, not what.
Keep polling
Section titled “Keep polling”Notifications are best effort, and a client that relies on them alone will miss mail:
- The server sends notifications from a bounded pool of workers (
performance.worker_threads, 4 by default). In a burst, notifications beyond that are dropped, not queued. The server’s own configuration comment says it: clients poll regardless. - A client that is offline, or whose connection dropped, gets nothing. Messages that arrived meanwhile are only found by retrieving.
- GossipSub delivery is not guaranteed.
So retrieve when you start, when you reconnect, on every notification, and on a timer. A notification shortens the delay; polling guarantees you eventually see everything.
The operator can turn push off entirely with features.enable_push_delivery. See
config.yaml.