Skip to content

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.

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.New
if 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.

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.