---
title: Social graph
description: 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.

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

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

```ts
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`):

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

```ts
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:

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

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

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

```ts
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](/reference/capabilities) 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](/platforms/youtube#update-channel-details).
- **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.
