Skip to content

A public board

This recipe builds a public board in the spirit of a Usenet newsgroup: one peer hosts it, posts are readable by anyone, and readers who are online hear about new posts at once. Ricochet offers two building blocks for it, and this page shows both.

Public mailbox Collaborative feed
Who may post The owner, and peers granted write access Anyone
Who may read Anyone Anyone (feeds are public by default)
Push to readers Yes, on GossipSub No; readers poll
Reader position Kept by the server per reader Kept by the reader (sequence number)
Lifetime of a post Message expiry and mailbox retention Until the feed is deleted, up to the per-feed entry cap
Moderation Grant and revoke posters None; filter on CreatedBy when reading

The owner creates a public mailbox and grants posting rights:

// Sketch: the board owner's setup.
err := owner.CreateMailbox(ctx, "boards/gardening", wire.MailboxPublic,
client.WithMailboxMaxMessages(1000),
client.WithRetentionDays(30),
client.WithRetentionCount(900), // keep the newest 900 when the cap is reached
)
err = owner.GrantAccess(ctx, "boards/gardening", posterID, wire.AccessWriteOnly)

WithRetentionCount below the cap makes the board a rolling window: when it fills, the server prunes the oldest posts instead of refusing new ones.

A poster sends a message to the owner’s folder. Posts are public, so they are not encrypted; make them persistent, and give them an expiry as long as the board’s retention, because the default expiry is seven days:

// Sketch: posting.
post, _ := json.Marshal(map[string]string{"subject": "Tomatoes", "body": "Started seeds today."})
res, err := poster.SendMessage(ctx, ownerID, post,
client.WithFolderPath("boards/gardening"),
client.WithPersistent(true),
client.WithExpiry(30*24*time.Hour),
)
if err == nil {
err = res.Err() // client.ErrForbidden without a grant
}

A reader subscribes to the board’s GossipSub topic for push, and retrieves. The server keeps a cursor per reader on a public mailbox, so each retrieve without WithFromSequence returns only posts that reader has not retrieved yet:

// Sketch: a reader. h is the reader's host, connected to the board's server.
ps, err := pubsub.NewGossipSub(ctx, h)
if err != nil {
return err
}
topic, err := ps.Join("/ricochet/mailbox/" + ownerID.String() + "/boards/gardening")
if err != nil {
return err
}
sub, err := topic.Subscribe()
if err != nil {
return err
}
fetch := func() {
page, err := reader.RetrievePage(ctx,
client.WithTargetPeer(ownerID),
client.WithRetrieveFolderPath("boards/gardening"))
for err == nil {
show(page.Messages) // your function
if !page.HasMore {
return
}
page, err = reader.RetrievePage(ctx,
client.WithTargetPeer(ownerID),
client.WithRetrieveFolderPath("boards/gardening"))
}
}
fetch() // catch up on start
for {
if _, err := sub.Next(ctx); err != nil {
return err
}
fetch()
}

To show the whole board again, for instance on a new device, retrieve with WithFromSequence(1). Readers cannot acknowledge or delete posts; the owner can, with DeleteMessages, which is how moderation removes a post. Poll on a timer as well, since GossipSub notifications are best effort.

The owner creates a collaborative feed, and anyone appends to it:

// Sketch: a feed-based board.
err := owner.CreateFeed(ctx, "boards/gardening", "Gardening", "Seeds, soil and weather",
client.WithCollaborative())
seq, err := poster.AppendToFeed(ctx, ownerID, "boards/gardening", post, "post")

Readers track the last sequence number themselves and poll:

entries, hasMore, err := reader.GetFeedEntries(ctx, ownerID, "boards/gardening",
client.WithFeedEntryFrom(lastSeq+1),
client.WithFeedEntryLimit(200),
)
for _, e := range entries {
show(e.CreatedBy, e.Content) // CreatedBy is the poster's peer ID, set by the server
lastSeq = e.Seq
}

A reader following many boards fetches them all in one request with GetMultiFeedEntries, one client.BatchFeedQuery per board with its own FromSequence. Use the entry type to separate posts from replies, and WithFeedEntryType to read only one kind.

A feed has no push. If readers need it, combine the two designs: the owner runs a small bot that follows the feed and sends a short persistent message to a public mailbox for each new entry, and readers subscribe to that mailbox’s topic.

Publish the board’s address (owner peer ID, folder or feed path, and the server’s address) wherever your users look. One option is the owner’s directory listing: put the board paths in Extras when calling JoinDirectory, and readers find them with BrowseDirectory. See Directory & presence.