Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/bluesky-social/atproto/llms.txt

Use this file to discover all available pages before exploring further.

@atproto/bsky

TypeScript implementation of the app.bsky Lexicons backing the Bluesky microblogging application. This package provides the AppView service that aggregates data from Personal Data Servers and serves the Bluesky API.

Installation

npm install @atproto/bsky

Overview

The Bluesky AppView is a service that:
  • Indexes data from multiple PDSs via the data plane
  • Provides the app.bsky.* Lexicon endpoints
  • Implements social graph algorithms (feeds, notifications, suggestions)
  • Manages content moderation and labeling
  • Handles blob/media serving and transformation
  • Provides search functionality
Architecture:
  • AppView: Public-facing API service
  • Data Plane: Backend indexing and data aggregation
  • Hydrator: Enriches data with additional context
  • Views: Formats data for API responses

Main Exports

BskyAppView Class

The main AppView server class.
import { BskyAppView, ServerConfig } from '@atproto/bsky'
import { Keypair } from '@atproto/crypto'

// Load configuration
const config = ServerConfig.readEnv()

// Generate signing key
const signingKey = await Keypair.create()

// Create AppView
const appView = BskyAppView.create({
  config,
  signingKey,
})

// Start server
await appView.start()

Methods

static create(opts: { config: ServerConfig; signingKey: Keypair }): BskyAppView Creates a new AppView instance. async start(): Promise<http.Server> Starts the AppView server and establishes connections to dependencies. async destroy(): Promise<void> Gracefully shuts down the server.

ServerConfig Class

Configuration management for the AppView.
import { ServerConfig } from '@atproto/bsky'

// Read from environment variables
const config = ServerConfig.readEnv()

// Access configuration
const port = config.port
const serverDid = config.serverDid
const dataplaneUrls = config.dataplaneUrls

AppContext

Application context with shared services.
import { AppContext } from '@atproto/bsky'

const ctx = appView.ctx

// Available services:
ctx.dataplane       // Data plane client
ctx.hydrator        // Data hydration
ctx.views           // View formatting
ctx.authVerifier    // Authentication
ctx.searchAgent     // Search service client
ctx.bsyncClient     // Sync service client

Configuration

ServerConfigValues

Complete configuration interface:
interface ServerConfigValues {
  // Service
  version?: string
  debugMode?: boolean
  port?: number
  publicUrl?: string
  serverDid: string
  alternateAudienceDids: string[]
  entrywayJwtPublicKeyHex?: string

  // External services
  etcdHosts: string[]
  dataplaneUrls: string[]
  dataplaneUrlsEtcdKeyPrefix?: string
  dataplaneHttpVersion?: '1.1' | '2'
  dataplaneIgnoreBadTls?: boolean
  bsyncUrl: string
  bsyncApiKey?: string
  searchUrl?: string
  suggestionsUrl?: string
  topicsUrl?: string

  // CDN and media
  cdnUrl?: string
  videoPlaylistUrlPattern?: string
  videoThumbnailUrlPattern?: string
  blobRateLimitBypassKey?: string
  blobRateLimitBypassHostname?: string

  // Identity
  didPlcUrl: string
  handleResolveNameservers?: string[]

  // Moderation
  modServiceDid: string
  adminPasswords: string[]
  labelsFromIssuerDids?: string[]

  // Threading
  bigThreadUris: Set<string>
  maxThreadDepth?: number
  maxThreadParents: number
  threadTagsHide: Set<string>
  threadTagsBumpDown: Set<string>

  // Feature flags
  eventProxyTrackingEndpoint?: string
  growthBookApiHost?: string
  growthBookClientKey?: string
  clientCheckEmailConfirmed?: boolean
  topicsEnabled?: boolean

  // HTTP proxy
  disableSsrfProtection?: boolean
  proxyAllowHTTP2?: boolean
  proxyHeadersTimeout?: number
  proxyBodyTimeout?: number
  proxyMaxResponseSize?: number
}

Environment Variables

Service Configuration

BSKY_VERSION=0.0.1
BSKY_PORT=2584
BSKY_PUBLIC_URL=https://api.bsky.app
BSKY_SERVER_DID=did:web:api.bsky.app

Data Plane

# Static URLs
BSKY_DATAPLANE_URLS=http://dataplane-1:2582,http://dataplane-2:2582

# Or dynamic discovery via etcd
BSKY_ETCD_HOSTS=http://etcd-1:2379,http://etcd-2:2379
BSKY_DATAPLANE_URLS_ETCD_KEY_PREFIX=/bsky/dataplane/urls

# HTTP version
BSKY_DATAPLANE_HTTP_VERSION=2
BSKY_DATAPLANE_IGNORE_BAD_TLS=false

Sync Service (Bsync)

BSKY_BSYNC_URL=http://bsync:2586
BSKY_BSYNC_API_KEY=secret-api-key
BSKY_BSYNC_HTTP_VERSION=2

External Services

# Search
BSKY_SEARCH_URL=http://search:2585

# Suggestions
BSKY_SUGGESTIONS_URL=http://suggestions:2587
BSKY_SUGGESTIONS_API_KEY=suggestions-api-key

# Topics
BSKY_TOPICS_URL=http://topics:2588
BSKY_TOPICS_API_KEY=topics-api-key

CDN and Media

BSKY_CDN_URL=https://cdn.bsky.app
BSKY_VIDEO_PLAYLIST_URL_PATTERN=https://video.bsky.app/watch/%s/%s/playlist.m3u8
BSKY_VIDEO_THUMBNAIL_URL_PATTERN=https://video.bsky.app/watch/%s/%s/thumbnail.jpg
BSKY_BLOB_CACHE_LOC=/tmp/blob-cache

Identity

BSKY_DID_PLC_URL=https://plc.directory
BSKY_HANDLE_RESOLVE_NAMESERVERS=8.8.8.8,1.1.1.1

Moderation

MOD_SERVICE_DID=did:plc:moderation
BSKY_ADMIN_PASSWORDS=admin-password-1,admin-password-2
BSKY_LABELS_FROM_ISSUER_DIDS=did:plc:labeler1,did:plc:labeler2

Threading

BSKY_BIG_THREAD_URIS=at://did:plc:abc/app.bsky.feed.post/123
BSKY_BIG_THREAD_DEPTH=20
BSKY_MAX_THREAD_DEPTH=1000
BSKY_MAX_THREAD_PARENTS=50
BSKY_THREAD_TAGS_HIDE=spam,sensitive
BSKY_THREAD_TAGS_BUMP_DOWN=low-quality

Feature Flags

BSKY_CLIENT_CHECK_EMAIL_CONFIRMED=true
BSKY_TOPICS_ENABLED=true
BSKY_EVENT_PROXY_TRACKING_ENDPOINT=http://analytics:9000

GrowthBook (A/B Testing)

BSKY_GROWTHBOOK_API_HOST=https://growthbook-api.example.com
BSKY_GROWTHBOOK_CLIENT_KEY=sdk-abc123

Setup Example

import { BskyAppView, ServerConfig } from '@atproto/bsky'
import { Keypair } from '@atproto/crypto'

async function main() {
  // Load configuration from environment
  const config = ServerConfig.readEnv()

  // Create signing keypair
  const signingKeyHex = process.env.BSKY_SIGNING_KEY_HEX
  const signingKey = signingKeyHex
    ? await Keypair.import(signingKeyHex)
    : await Keypair.create()

  // Create AppView
  const appView = BskyAppView.create({
    config,
    signingKey,
  })

  // Start server
  const server = await appView.start()
  const address = server.address()
  const port = typeof address === 'string' ? address : address?.port

  console.log(`Bluesky AppView listening on port ${port}`)

  // Graceful shutdown
  const shutdown = async () => {
    console.log('Shutting down AppView...')
    await appView.destroy()
    process.exit(0)
  }

  process.on('SIGTERM', shutdown)
  process.on('SIGINT', shutdown)
}

main().catch((err) => {
  console.error('Fatal error:', err)
  process.exit(1)
})

Data Plane Client

The AppView communicates with the data plane for indexed data:
import { createDataPlaneClient } from '@atproto/bsky'

const dataplane = createDataPlaneClient(
  dataplaneHostList,
  {
    httpVersion: '2',
    rejectUnauthorized: true,
  }
)

// Access from context
const dataplane = appView.ctx.dataplane

// Query data
const actor = await dataplane.getActor({ did: 'did:plc:abc123' })
const posts = await dataplane.getTimeline({ did: 'did:plc:abc123' })

Hydrator

The hydrator enriches data with additional context:
import { Hydrator } from '@atproto/bsky'

const hydrator = new Hydrator(
  dataplane,
  labelsFromIssuerDids,
  {
    debugFieldAllowedDids,
  }
)

// Hydrate actor profiles
const actors = await hydrator.hydrateActors(
  ['did:plc:abc123', 'did:plc:def456']
)

// Hydrate posts
const posts = await hydrator.hydratePosts(
  ['at://did:plc:abc/app.bsky.feed.post/123']
)

Views

Views format hydrated data for API responses:
import { Views } from '@atproto/bsky'

const views = new Views({
  imgUriBuilder,
  videoUriBuilder,
  indexedAtEpoch,
  threadTagsBumpDown,
  threadTagsHide,
})

// Format actor profile
const profileView = views.profile(actorState, viewerDid)

// Format post
const postView = views.post(postState, viewerDid)

// Format thread
const threadView = views.thread(threadState, viewerDid)

Authentication

The AppView validates requests using JWT tokens:
import { AuthVerifier } from '@atproto/bsky'

const authVerifier = new AuthVerifier(
  dataplane,
  {
    ownDid: config.serverDid,
    alternateAudienceDids: config.alternateAudienceDids,
    modServiceDid: config.modServiceDid,
    adminPasses: config.adminPasswords,
  }
)

// Verify request
const auth = await authVerifier.verifyAccessToken(req)

Feature Gates

A/B testing and feature flags via GrowthBook:
import { FeatureGatesClient } from '@atproto/bsky'

const featureGates = new FeatureGatesClient({
  growthBookApiHost: config.growthBookApiHost,
  growthBookClientKey: config.growthBookClientKey,
})

// Start client
featureGates.start()

// Check feature
const enabled = featureGates.isOn('new-feature', { did })

// Get variant
const variant = featureGates.getVariant('experiment-name', { did })

API Routes

The AppView implements all app.bsky.* Lexicons:
// Feed endpoints
app.bsky.feed.getTimeline
app.bsky.feed.getAuthorFeed
app.bsky.feed.getFeed
app.bsky.feed.getFeedGenerator
app.bsky.feed.getFeedGenerators
app.bsky.feed.getLikes
app.bsky.feed.getPostThread
app.bsky.feed.getPosts
app.bsky.feed.getRepostedBy

// Actor/Profile endpoints
app.bsky.actor.getProfile
app.bsky.actor.getProfiles
app.bsky.actor.searchActors
app.bsky.actor.getSuggestions

// Graph endpoints
app.bsky.graph.getFollowers
app.bsky.graph.getFollows
app.bsky.graph.getList
app.bsky.graph.getLists
app.bsky.graph.getListMutes

// Notification endpoints
app.bsky.notification.listNotifications
app.bsky.notification.getUnreadCount

Blob Serving

The AppView serves images and videos:
import { ImageUriBuilder, VideoUriBuilder } from '@atproto/bsky'

// Image URLs
const imgUriBuilder = new ImageUriBuilder('https://cdn.bsky.app')
const imageUrl = imgUriBuilder.getSignedPath({
  did: 'did:plc:abc123',
  cid: 'bafyreib2rxk3rh6kzwq',
  format: 'jpeg',
  fit: 'cover',
  width: 1000,
  height: 1000,
})

// Video URLs
const videoUriBuilder = new VideoUriBuilder({
  playlistUrlPattern: 'https://video.bsky.app/%s/%s/playlist.m3u8',
  thumbnailUrlPattern: 'https://video.bsky.app/%s/%s/thumbnail.jpg',
})
const playlistUrl = videoUriBuilder.getPlaylistUrl(did, cid)
const thumbnailUrl = videoUriBuilder.getThumbnailUrl(did, cid)

Database (Data Plane)

The data plane manages the indexed database:
import { Database } from '@atproto/bsky'

const db = Database.postgres({
  url: process.env.DATABASE_URL,
  schema: 'public',
  poolSize: 20,
})

// Transactions
await db.transaction(async (dbTxn) => {
  // Database operations
})

Resources

Build docs developers (and LLMs) love