Skip to content
Social SDK megaphone markSocial SDK
Esc
navigateopen⌘Jpreview
On this page

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.

Instagram

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 the app.bsky.actor.profile record, so send the complete record.
  • YouTube: native.updateProfile({ part, value, context }) updates the channel’s brandingSettings.channel fields or localizations. 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.

Last updated on September 24, 2026