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 take the guarded native module and an operation context:
const native = social.native("default", { acknowledgeUnsafe: true });
if (native === undefined) throw new Error("This adapter has no native module");
const context: AdapterOperationContext = {
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. Profile lookups and follower and following
reads can use it when auth.accessToken is absent. Blocked and muted lists and every mutation
need a user token with the scopes above and fail 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. getProfile reads one user per call. Relationship pages keep X user IDs as
profile refs and do not invent a since timestamp, and accept a limit of 1 to 100; the SDK does
not expose X’s larger page sizes. 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
need users.read and list.read. X returns all pinned lists in one response, so the method takes
no cursor or limit:
const pinned = await native.pinnedLists({ account, 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 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 (threads_profile_discovery). Under Standard Access, Meta
limits discovery to a small set of official accounts until your app has Advanced Access; see
Threads profiles.
const adapter = threads({ auth: { userId, accessToken } });For the authorized user, the adapter returns the profile ID, username, name, profile picture, biography, and verification data where present.
Handle lookup calls GET /profile_lookup with an exact username and requests only username,
name, profile_picture_url, biography, and is_verified. Meta returns only public profiles
with at least 100 followers. The response has no user ID, so the record’s
ref.profileId is the synthetic value lookup:<username> and native._profileIdUnavailable is
true. That ID is not a Threads user ID; do not pass it to other Threads calls.
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. The scopes depend on the login flavor:
- Instagram Login (default):
instagram_business_basicandinstagram_business_manage_insights. - Facebook Login (
auth.flavor: "facebook-login"):instagram_basic,instagram_manage_insights, andpages_read_engagement.
const adapter = instagram({ auth: { accessToken, accountId } });
const own = await social.graph.getProfile(account, {});getProfile accepts three selectors. {} reads the connected account. { profileId } reads an
Instagram user ID your token can access. { handle } uses business discovery to read another
professional account by username; it needs Facebook Login and raises unsupported_capability
under Instagram Login. A profile record contains the normalized ID, username, name, biography, and
profile image, with provider fields in native. Relationship reads and mutations raise
unsupported_capability. See Meta’s
business discovery
guide for eligibility.
YouTube
Supported: no normalized graph. Subscriptions are native. Listing needs
https://www.googleapis.com/auth/youtube.readonly; subscribing and unsubscribing need
https://www.googleapis.com/auth/youtube or youtube.force-ssl.
Read or change subscriptions through the native adapter. The response is provider JSON, including
nextPageToken when more results are available. Without channelId, list returns the
authorized user’s own subscriptions:
const mine = await native.subscriptions({ action: "list", context });Passing channelId lists the channels that channel subscribes to, which YouTube returns only when
that channel’s subscriptions are public:
const theirs = await native.subscriptions({ action: "list", channelId: "UC_other", 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.