Social graph
Read profiles, followers, and following lists, then follow, block, or mute accounts through the normalized graph API or each platform's native module.
Use social.graph when you need a provider-neutral profile or relationship operation. The facade
is capability checked: it does not turn a provider’s feed or native response into a relationship
that the adapter did not declare.
const profile = await social.graph.getProfile(account, { handle: "someone" });
const page = await social.graph.listRelationships(account, { kind: "followers", limit: 50 });
await social.graph.follow(profile.ref);
getProfile returns a ProfileRecord with a stable ref plus optional displayName, handle,
avatarUrl, bio, and provider data in native. listRelationships returns
Page<RelationshipRecord>: items contain a profile reference and relationship (following,
follower, blocked, or muted), and nextCursor is an opaque provider cursor. Pass that
cursor to another call with the same account and kind; the facade has no graph.iterate().
The mutation methods are follow, unfollow, mute, unmute, block, and unblock.
Setup
Configure one adapter and one connected-account reference on your server. The platform tabs below
reuse social and account and show only what differs.
const social = createSocial({
backend: x({ auth: { userId, accessToken }, appBearerToken: process.env.X_APP_BEARER_TOKEN }),
});
const account = connectedAccountRef({ backend: "default", platform: "x", accountId: userId });
Native calls also take an operation context, for example
{ backendInstance: "default", correlationId: "graph", retryBudget: { maxAttempts: 2, maxElapsedMs: 30_000 } }.
Platform support
Adapters without a tab here do not implement this facade. The client rejects an unavailable
operation with SocialError code unsupported_capability before adapter I/O.
X
Supported: profile; followers, following, blocked, and muted lists; follow, unfollow, mute,
unmute, block, and unblock. Scopes: users.read, follows.read/follows.write,
mute.read/mute.write, block.read/block.write. Block and unblock are Enterprise-plan only.
Configure x() with the connected user’s ID and, for user-context operations, an access token.
appBearerToken is a separate app-only credential, used for profile and relationship reads when
auth.accessToken is absent. Every mutation needs a user token and fails with
missing_permission without one.
const followers = await social.graph.listRelationships(account, { kind: "followers", limit: 100 });Profile records normalize the user ID, name, username, and description, with the validated user
object under native. Relationship pages keep X user IDs as profile refs and do not invent a
since timestamp. User reads accept 1 to 1,000 items; relationship endpoints accept 1 to 100. An
invalid size raises invalid_input. A revoked or expired user token raises reconnect_required.
X lists are native only: owned lists, list members, list following, and pinned lists. Pinned reads
use lists.pinned.read (users.read, list.read):
const native = social.native("default", { acknowledgeUnsafe: true });
const pinned = await native.pinnedLists({ account, limit: 25, context });Bluesky
Supported: profile; followers, following, blocked, and muted lists; all six mutations.
Access: repo, through an access JWT or OAuth session.
Configure bluesky() with the PDS service, the account DID, and either an accessJwt or an
OAuth session, not both. Profile lookup accepts handles and DIDs; normalized profile refs use
the DID.
const adapter = bluesky({ auth: { service: "https://bsky.social", did, accessJwt } });Records normalize DID, handle, display name, avatar, and description. Relationship reads map
app.bsky.graph.getFollowers, getFollows, getMutes, and getBlocks, and cursors pass through
unchanged. unfollow first reads viewer.following and unblock reads viewer.blocking; if that
URI is absent the call fails with invalid_input. Mute and unmute use actor DIDs.
Bluesky lists are native only and need repo. The native surface covers list creation, member
changes, list reads, and list mute and block:
const native = social.native("default", { acknowledgeUnsafe: true });
const lists = await native.getLists({ account, actor: did, limit: 50, context });Threads
Supported: profile only. Scope: threads_basic for profiles.read.
Configure the adapter with the authorized Threads user ID and access token. Handle lookup also depends on Threads profile discovery; without it, lookup is limited to the authorized app-scoped user.
const adapter = threads({ auth: { userId, accessToken } });The adapter returns profile ID, username, name, profile picture, biography, and verification data
where present. Handle lookup calls GET /profile_lookup, which needs an exact username match and
only returns public profiles with at least 100 followers. Its follower_count and seven-day
engagement counts stay in native.
Threads has no endpoint that lists followers or followed accounts, so the manifest declares
graph.read as unsupported-by-platform, and listRelationships raises unsupported_capability
before any request. Mutations raise the same error. For the authorized user’s total follower
count, read the followers_count metric with social.analytics.getAccountMetrics.
Supported: profile only. Scopes: instagram_business_basic and
instagram_business_manage_insights, on a professional account.
instagram() uses Instagram Login by default. Set auth.flavor to "facebook-login" and use a
Facebook Login token when you need business-discovery lookup by handle.
const adapter = instagram({ auth: { accessToken, accountId } });
const own = await social.graph.getProfile(account, {});A profile record contains the normalized ID, username, name, biography, and profile image, with
provider fields in native. Handle lookup validates the handle. Relationship reads and mutations
raise unsupported_capability.
YouTube
Supported: no normalized graph. Subscriptions are native and need
https://www.googleapis.com/auth/youtube.readonly.
Read or change subscriptions through the native adapter. The response is provider JSON, including
nextPageToken when more results are available.
const native = social.native("default", { acknowledgeUnsafe: true });
const subscriptions = await native.subscriptions({ action: "list", channelId, context });Profile updates
Profile writes are native only, because each platform accepts different fields. Check the
profile.update declaration in the capability matrix before you offer
the feature.
- Bluesky:
native.updateProfile({ account, profile, context })replaces theapp.bsky.actor.profilerecord, so send the complete record. - YouTube:
native.updateProfile({ part, value, context })updates the channel’sbrandingSettings.channelfields orlocalizations. See the YouTube guide. - LinkedIn: member profile writes use the Profile Edit API, which LinkedIn limits to approved developers. The adapter does not implement it.
- X, Threads, Instagram, and TikTok: the platform APIs have no profile write, and the
operation is declared
unsupported-by-platform.
Errors and authorization
Catch SocialError at the application boundary and branch on code, not provider message text.
Useful codes here are unsupported_capability, missing_permission, reconnect_required,
unauthorized, invalid_input, not_found, rate_limited, and upstream_failure. Treat an
ambiguous_outcome as uncertain rather than claiming that a follow, mute, or block succeeded.
Native calls bypass tenant authorization and concurrency middleware.