Skip to content

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

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)
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.

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.

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 server
topic, 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.