Directory & presence
This page covers two ways to find other peers through a server: the directory, a public list of profiles that peers opt into, and presence, the server’s view of who is connected right now.
The directory
Section titled “The directory”The directory belongs to one server. A peer joins it on a server by publishing a listing; any peer can browse and search every listing on that server. There is no visibility setting: a listing is public.
Join or update your listing
Section titled “Join or update your listing”err := cl.JoinDirectory(ctx, client.DirectoryListing{ DisplayName: "Ann Example", Bio: "Writes about gardens.", AvatarHash: "sha256:…", // optional, your app's reference to an image Extras: map[string]any{"lang": "en"},})DisplayName is required. Limits: display name wire.MaxDisplayNameLength (256 bytes), bio
wire.MaxBioLength (4096), avatar hash wire.MaxAvatarHashLength (256), and extras
wire.MaxDirectoryExtrasBytes (4096 bytes as JSON). Calling JoinDirectory again replaces your
listing, so it is also the update call.
The listing can also come from a document. A PutDocument or PatchDocument of your own document at the
path directory-listing, with JSON fields displayName, bio, avatarHash and extras, updates your
listing; "listed": false in it, or deleting the document, removes you. This lets an app keep the profile
as an ordinary document and sync it like the rest.
err = cl.LeaveDirectory(ctx)Browse and search
Section titled “Browse and search”res, err := cl.BrowseDirectory(ctx, client.WithDirectoryQuery("garden"), client.WithDirectoryLimit(50),)for _, e := range res.Entries { fmt.Println(e.OwnerPeerID, e.DisplayName, e.Bio)}if res.HasMore { res, err = cl.BrowseDirectory(ctx, client.WithDirectoryQuery("garden"), client.WithDirectoryCursor(res.NextCursor))}| Option | Effect |
|---|---|
WithDirectoryQuery(q) |
Full-text search over display name and bio, matching word prefixes. Without it, browse every listing. |
WithDirectoryLimit(n) |
Page size. Default 20, maximum 100. |
WithDirectoryCursor(c) |
Continue from the previous page’s NextCursor. |
WithDirectoryServer(id) |
Browse one server’s directory. |
A cursor or query the server cannot read is refused with status 400.
Look up one peer
Section titled “Look up one peer”entry, err := cl.GetDirectoryEntry(ctx, peerID)if err == nil && entry == nil { // not listed on this server}JoinDirectory, LeaveDirectory and GetDirectoryEntry take WithDocServer to address one server.
Presence
Section titled “Presence”The server tracks which peers are connected to it, and uses that to decide whom to push notifications to.
A peer is online while it holds a connection to the server. pkg/client has no presence call; the server
publishes presence on GossipSub, and you read it with your own GossipSub router on the same host.
The topic is /sf-network/presence/<server peer ID>. Two kinds of JSON message are published on it, told
apart by their type field:
type |
When | Fields |
|---|---|---|
event |
A peer connects or disconnects (batched over a short window) | serverId, timestamp, changes: a list of peerId, state, ttlSeconds |
heartbeat |
Every intervals.presence_heartbeat_sec (60 s by default) |
serverId, timestamp, onlineCount, onlinePeerIds, heartbeatSequence, page, pageCount |
state is a number: 0 online, 1 probably online, 2 offline, 3 unknown. ttlSeconds on an online change
is how long you may trust it without a heartbeat confirming it (intervals.presence_timeout_sec, 120 s
by default). A large online set is split across heartbeat pages that share one heartbeatSequence.
// Sketch: subscribe to a server's presence topic with go-libp2p-pubsub.ps, err := pubsub.NewGossipSub(ctx, h) // h is connected to the servertopic, err := ps.Join("/sf-network/presence/" + serverID.String())sub, err := topic.Subscribe()for { msg, err := sub.Next(ctx) if err != nil { return err } var ev struct { Type string `json:"type"` Changes []struct { PeerID string `json:"peerId"` State int `json:"state"` } `json:"changes"` OnlinePeerIDs []string `json:"onlinePeerIds"` } if json.Unmarshal(msg.Data, &ev) == nil { updateRoster(ev.Type, ev.Changes, ev.OnlinePeerIDs) // your function }}The operator can turn presence broadcasting off (features.enable_presence_broadcast), in which case the
topic stays silent. Presence only describes peers connected to that server; a peer online elsewhere looks
offline here. See config.yaml.