Use server+client ephemeral joins with longer flush for CONTACT_REQUEST delivery. Forward DM_CHANNEL_UPSERT to peers on the contacts mesh. Co-authored-by: Cursor <[email protected]>
pearcord-contacts
P2P friend requests, blocks, and friend presence on the contacts Hyperswarm mesh.
Mission
Maintain the signed-in user's contact graph in JsonStore (pending_in/out, accepted, blocked), gossip request/accept/decline/remove/block RPCs to peer-specific contactsTopic buffers, join friend topics for ongoing PRESENCE_UPDATE and profile cosmetic sync, and expose friend presence map for DM sidebar and activity feed.
v0.8.557 (Phase 594): UI #contacts-hub-panel-live + syncContactsHubLivePanelDuringGuildLoading; contacts-errors excludes audit/compliance/slash/webhook/bot/automation/attachment/embed/message/reaction/pin/search/voice/stage/screen/soundboard/presence/discovery; clipboard contactsHubPanelLiveBusy; platform guildCount on contact span ends; agentctl blockContact + refreshView re-verify. Bundle: test:phase594-contacts (app repo).
v0.8.527 (Phase 564): UI #contacts-panel-live + syncContactsLivePanelDuringGuildLoading; dev-log contacts meta extend; agentctl send/accept refreshView re-verify. Bundle: npm run test:phase564-contacts (app repo).
v0.8.509 (Phase 546): Platform contacts.block/contacts.unblock spans + activeChannelId on all contact span ends; split panel compositor (modal backdrop/form, pending in/out, blocked list); agentctl refreshView after contacts ops. Bundle: npm run test:phase546-contacts.
v0.8.493 (Phase 530): Platform span ends add pendingOutCount/acceptedCount/pendingInCount; UI compositor panels syncContactsModalPanelDuringGuildLoading, syncContactsPendingPanelDuringGuildLoading, syncContactsFriendPanelDuringGuildLoading; anchored contacts-errors filter. Bundle: npm run test:phase530-contacts.
v0.8.479 (Phase 516): Platform logs contacts.request/accept/decline with spanKind metadata; UI syncContactsPanelsDuringGuildLoading + contacts-errors dev-log filter. Bundle: npm run test:phase516-contacts. See CONTACTS.md.
When to use / not
Use when:
- You need friend list operations in isolation from guild discovery.
- You are running contacts mesh live smokes with two Hyperswarm peers.
Do not use when:
- You need guild member lists — use
pearcord-guildroster. - You need notification inbox — use
pearcord-notifications(separate settings topic).
Public API
| Export | Role |
|---|---|
PearcordContacts |
EventEmitter mesh + store |
CONTACT_STATUS |
pending_out, pending_in, accepted, blocked |
contactsTopic(userId) |
Topic string helper |
ready, list, get, listAccepted, listPending* |
CRUD reads |
sendRequest, acceptRequest, declineRequest, removeContact |
Outbound + gossip |
blockContact, unblockContact, listBlocked |
Block graph |
ingestRequest / ingestAccept / … |
Inbound handlers |
broadcastPresence, broadcastProfileCosmetic |
Fanout to friends |
ingestFriendPresence, getFriendPresenceMap |
Friend activity cache |
joinMesh(swarm) / leaveMesh |
contactsTopic(self) server+client |
simulateGossip, gossipLocal, getStats |
Tests |
Uses pearcord-drive mux wire via ./mesh (contacts Protomux protocol).
P2P surface
| Topic | Role |
|---|---|
contactsTopic(userId) |
Inbox for requests to that user |
| Friend topics | Joined for each accepted peer (client) |
| RPC | CONTACT_*, PRESENCE_UPDATE, PROFILE_COSMETIC_UPDATE |
Ephemeral topic join for one-shot request delivery when not yet friends.
Storage
| Collection | Path |
|---|---|
@pearcord/contacts |
{storagePath}/contacts |
Primary key: { ownerId, peerUserId }. Friend presence is in-memory _friendPresence Map.
Platform integration
const { PearcordContacts } = require('pearcord-contacts')
this.contacts = new PearcordContacts({ userId, username, displayName, storagePath })
await this.contacts.ready()
await this._joinContactsMesh() // dedicated Hyperswarm + joinMesh
// friend-request IPC → sendRequest / acceptRequest
// setPresence → contacts.broadcastPresence after presence.setStatus
Platform README lists contactsTopic alongside dmTopic in mesh table.
UI / IPC
| IPC | Platform path |
|---|---|
send-friend-request |
sendRequest |
accept-friend-request |
acceptRequest |
list-contacts |
accepted + pending + blocked + presence map |
| Friends activity | getFriendPresenceMap + buildActivityFeed |
Related docs
Tests
smoke-contacts-mesh-live.cjssmoke-runner.cjs—contactsTopicassertion- Platform friend flows in onboarding smokes
- v0.8.442 (Phase 479): bundled in
test:phase479-discovery; platformcontacts.acceptaccepted; UIsetDmRowCurrent. - v0.8.431 (Phase 468): bundled in
test:phase468-discovery-dm;test:contacts-guild-loading(.friend-itemguards); platformcontacts.acceptspan.endpeerUserId+error:key; clipboardcontactsPendingCount/dmChannelCount; UI#composer-dm-hint+ 🔒 E2E badge - v0.8.396 (Phase 433): bundled in
test:phase433-discovery-dm;test:contacts-modal-escape-focus; platformcontacts.* errorlogs for dm-errors filter
Code example
const { PearcordContacts, CONTACT_STATUS } = require('pearcord-contacts')
const Hyperswarm = require('hyperswarm')
const contacts = new PearcordContacts({
userId: 'alice',
username: 'alice',
storagePath: './pearcord-storage'
})
await contacts.ready()
await contacts.joinMesh(new Hyperswarm())
const row = await contacts.sendRequest({
peerUserId: 'bob',
peerDisplayName: 'Bob'
})
console.log(row.status === CONTACT_STATUS.PENDING_OUT)
Repository
Part of Pearcord.
- Org:
pearcord - Clone:
git clone https://git.ssh.surf/pearcord/pearcord-contacts.git - Install:
npm install git+https://git.ssh.surf/pearcord/pearcord-contacts.git#main