Show profiles in your app
@omote-social/profiles resolves how an account appears in your app: it reads your profile record and every record it extends from the account’s own server, and folds them. No AppView is involved, Bluesky’s or Omote’s.
Install the library
Section titled “Install the library”npm install @omote-social/profilesIt works in browsers, Node 22 or later, and Cloudflare Workers.
Resolve a profile
Section titled “Resolve a profile”Pass an account (a handle or DID) and your app’s profile collection to getProfile():
import { getProfile } from "@omote-social/profiles";
const profile = await getProfile("alice.example.com", "social.grain.actor.profile");The result holds the resolved fields and where each came from:
{ "did": "did:plc:…", "handle": "alice.example.com", "collection": "social.grain.actor.profile", "displayName": "Alice Mori", "avatar": "https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=…&cid=…", "sources": { "displayName": { "collection": "social.omote.actor.profile" }, "description": { "collection": "social.grain.actor.profile", "hidden": true }, "avatar": { "collection": "app.bsky.actor.profile" } }, "chain": ["app.bsky.actor.profile", "social.omote.actor.profile", "social.grain.actor.profile"]}Display the result
Section titled “Display the result”- Show the handle when there is no name.
displayNameis absent when no record in the chain has one, or when one hides it. - Load images from the URLs given.
avatarandbannerare served by the account’s own server, not by any one app’s CDN. - Treat every value as untrusted text. The library already drops a
websitethat isn’t anhttp:orhttps:link, but other fields are shown as the person wrote them.
Choose a default for new people
Section titled “Choose a default for new people”Someone who has never used your app has no record in it. getProfile() then resolves as if their record existed with defaultExtends, which is Bluesky’s profile followed by the shared profile unless you pass another:
await getProfile(actor, "social.grain.actor.profile", { defaultExtends: ["social.omote.actor.profile"],});Handle failures
Section titled “Handle failures”getProfile() throws a ProfileError whose code says what went wrong. A missing profile is not an error, but an unreachable server is:
import { getProfile, ProfileError } from "@omote-social/profiles";
try { return await getProfile(actor, collection);} catch (error) { if (error instanceof ProfileError && error.code === "RecordUnavailable") { // The account's server didn't answer. Try again later. } throw error;}See the library reference for every code.
Resolve records you already have
Section titled “Resolve records you already have”If your app already stores the records, for example from the firehose, resolve them without the network:
import { resolveProfile } from "@omote-social/profiles";
const records = new Map([ ["app.bsky.actor.profile", bskyRecord], ["social.grain.actor.profile", grainRecord],]);
const { fields, sources, chain } = resolveProfile(records, "social.grain.actor.profile");resolveProfile() applies exactly the same rules as getProfile().
Use the endpoint instead
Section titled “Use the endpoint instead”For prototyping, omote.social answers the same question over XRPC. See the resolve endpoint. In production, resolve with the library so no service sits between your app and the account.