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/pds

TypeScript reference implementation of an atproto Personal Data Server (PDS). The PDS is responsible for hosting user data, managing identities, and coordinating with the wider AT Protocol network.
If you are interested in self-hosting a PDS for production use, see the official PDS distribution repository which includes deployment configuration, documentation, and Docker support.

Installation

npm install @atproto/pds

Overview

The PDS package provides the core server implementation for hosting user repositories, managing authentication, handling blob storage, and synchronizing data with AppViews and other services in the AT Protocol network. Key responsibilities:
  • User account management and authentication
  • Repository hosting and data storage
  • Blob (media) storage and management
  • Event sequencing and synchronization
  • Identity resolution (DIDs and handles)
  • OAuth provider implementation
  • Email verification and invites

Main Exports

PDS Class

The main server class that orchestrates all PDS functionality.
import { PDS, ServerConfig, ServerSecrets } from '@atproto/pds'

const config: ServerConfig = {
  service: {
    port: 2583,
    hostname: 'pds.example.com',
    publicUrl: 'https://pds.example.com',
    did: 'did:web:pds.example.com',
    devMode: false,
    blobUploadLimit: 5 * 1024 * 1024, // 5MB
    acceptingImports: true,
  },
  // ... additional configuration
}

const secrets: ServerSecrets = {
  jwtSecret: 'your-jwt-secret',
  plcRotationKey: 'your-plc-rotation-key',
}

const pds = await PDS.create(config, secrets)
await pds.start()

Methods

static async create(config: ServerConfig, secrets: ServerSecrets, overrides?: Partial<AppContextOptions>): Promise<PDS> Creates a new PDS instance with the provided configuration. async start(): Promise<http.Server> Starts the PDS server and begins listening for requests. async destroy(): Promise<void> Gracefully shuts down the server and cleans up resources.

AppContext

The application context contains all shared services and dependencies.
import { AppContext } from '@atproto/pds'

// Access from PDS instance
const ctx = pds.ctx

// Available services:
ctx.db              // Database access
ctx.accountManager  // User account management
ctx.actorStore      // Repository storage
ctx.sequencer       // Event sequencing
ctx.blobstore       // Blob storage
ctx.idResolver      // Identity resolution

Database

Database interface for PDS data access.
import { Database } from '@atproto/pds'

const db = ctx.db

// Transaction support
await db.transaction(async (dbTxn) => {
  // Perform database operations
})

DiskBlobStore

Disk-based blob storage implementation.
import { DiskBlobStore } from '@atproto/pds'

const blobstore = new DiskBlobStore({
  location: '/path/to/blobs',
  tempLocation: '/path/to/tmp',
})

Configuration

ServerConfig

Complete server configuration interface.
interface ServerConfig {
  service: ServiceConfig
  db: DatabaseConfig
  actorStore: ActorStoreConfig
  blobstore: S3BlobstoreConfig | DiskBlobstoreConfig
  identity: IdentityConfig
  entryway: EntrywayConfig | null
  invites: InvitesConfig
  email: EmailConfig | null
  moderationEmail: EmailConfig | null
  subscription: SubscriptionConfig
  bskyAppView: BksyAppViewConfig | null
  modService: ModServiceConfig | null
  reportService: ReportServiceConfig | null
  redis: RedisScratchConfig | null
  rateLimits: RateLimitsConfig
  crawlers: string[]
  fetch: FetchConfig
  proxy: ProxyConfig
  oauth: OAuthConfig
  lexicon: LexiconResolverConfig
}

ServiceConfig

Core service configuration.
interface ServiceConfig {
  port: number                  // Server port (default: 2583)
  hostname: string              // Public hostname
  publicUrl: string             // Full public URL
  did: string                   // Service DID
  version?: string              // Service version
  privacyPolicyUrl?: string     // Privacy policy URL
  termsOfServiceUrl?: string    // Terms of service URL
  acceptingImports: boolean     // Accept repo imports
  maxImportSize?: number        // Max import size in bytes
  blobUploadLimit: number       // Max blob upload size
  contactEmailAddress?: string  // Contact email
  devMode: boolean              // Development mode
}

DatabaseConfig

SQLite database configuration.
interface DatabaseConfig {
  accountDbLoc: string              // Account database path
  sequencerDbLoc: string            // Sequencer database path
  didCacheDbLoc: string             // DID cache database path
  disableWalAutoCheckpoint: boolean // Disable WAL auto-checkpoint
}

Blobstore Configuration

S3 Blobstore

interface S3BlobstoreConfig {
  provider: 's3'
  bucket: string
  region?: string
  endpoint?: string
  forcePathStyle?: boolean
  uploadTimeoutMs?: number
  credentials?: {
    accessKeyId: string
    secretAccessKey: string
  }
}

Disk Blobstore

interface DiskBlobstoreConfig {
  provider: 'disk'
  location: string       // Blob storage directory
  tempLocation?: string  // Temporary upload directory
}

IdentityConfig

Identity resolution configuration.
interface IdentityConfig {
  plcUrl: string                   // PLC directory URL
  resolverTimeout: number          // Resolution timeout (ms)
  cacheStaleTTL: number            // Cache stale TTL (ms)
  cacheMaxTTL: number              // Cache max TTL (ms)
  recoveryDidKey: string | null    // Recovery DID key
  serviceHandleDomains: string[]   // Allowed handle domains
  handleBackupNameservers?: string[] // DNS nameservers
  enableDidDocWithSession: boolean // Include DID doc in session
}

RateLimitsConfig

Rate limiting configuration.
type RateLimitsConfig =
  | {
      enabled: true
      bypassKey?: string    // Bypass header value
      bypassIps?: string[]  // Bypass IP addresses
    }
  | { enabled: false }

OAuthConfig

OAuth provider configuration.
interface OAuthConfig {
  issuer: string  // OAuth issuer URL
  provider?: {
    hcaptcha?: HcaptchaConfig
    branding: BrandingInput
    trustedClients?: string[]
  }
}

Environment Variables

The PDS reads configuration from environment variables:

Service

PDS_PORT=2583
PDS_HOSTNAME=pds.example.com
PDS_SERVICE_DID=did:web:pds.example.com
PDS_DEV_MODE=false
PDS_BLOB_UPLOAD_LIMIT=5242880  # 5MB in bytes

Database

PDS_DATA_DIRECTORY=/pds/data
PDS_ACCOUNT_DB_LOCATION=/pds/data/account.sqlite
PDS_SEQUENCER_DB_LOCATION=/pds/data/sequencer.sqlite
PDS_DID_CACHE_DB_LOCATION=/pds/data/did_cache.sqlite

Blobstore (S3)

PDS_BLOBSTORE_S3_BUCKET=my-pds-blobs
PDS_BLOBSTORE_S3_REGION=us-west-2
PDS_BLOBSTORE_S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

Blobstore (Disk)

PDS_BLOBSTORE_DISK_LOCATION=/pds/blobs
PDS_BLOBSTORE_DISK_TMP_LOCATION=/pds/tmp

Identity

PDS_DID_PLC_URL=https://plc.directory
PDS_SERVICE_HANDLE_DOMAINS=.pds.example.com
PDS_DID_CACHE_STALE_TTL=3600000   # 1 hour
PDS_DID_CACHE_MAX_TTL=86400000    # 1 day

Email

PDS_EMAIL_SMTP_URL=smtps://user:pass@smtp.example.com
PDS_EMAIL_FROM_ADDRESS=noreply@pds.example.com

Entryway (Optional)

PDS_ENTRYWAY_URL=https://entryway.example.com
PDS_ENTRYWAY_DID=did:web:entryway.example.com
PDS_ENTRYWAY_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEX=...
PDS_ENTRYWAY_PLC_ROTATION_KEY=...

Rate Limits

PDS_RATE_LIMITS_ENABLED=true
PDS_RATE_LIMIT_BYPASS_KEY=secret-bypass-key
PDS_RATE_LIMIT_BYPASS_IPS=127.0.0.1,10.0.0.0/8

Redis (Optional)

PDS_REDIS_SCRATCH_ADDRESS=redis://localhost:6379
PDS_REDIS_SCRATCH_PASSWORD=redis-password

Setup Example

Complete setup example:
import { PDS, envToCfg, readEnv } from '@atproto/pds'
import { Secp256k1Keypair } from '@atproto/crypto'

// Load configuration from environment
const env = readEnv()
const config = envToCfg(env)

// Generate or load signing key
const signingKey = await Secp256k1Keypair.create()

// Create secrets
const secrets = {
  jwtSecret: process.env.PDS_JWT_SECRET,
  plcRotationKey: process.env.PDS_PLC_ROTATION_KEY,
}

// Create and start PDS
const pds = await PDS.create(config, secrets)
const server = await pds.start()

console.log(`PDS listening on port ${config.service.port}`)

// Graceful shutdown
process.on('SIGTERM', async () => {
  console.log('Shutting down PDS...')
  await pds.destroy()
  process.exit(0)
})

Repository Operations

The PDS manages user repositories:
import { prepareCreate, prepareUpdate, prepareDelete } from '@atproto/pds/repo'

// Prepare a record creation
const write = await prepareCreate({
  did: 'did:plc:abc123',
  collection: 'app.bsky.feed.post',
  rkey: 'self',
  record: {
    text: 'Hello, AT Protocol!',
    createdAt: new Date().toISOString(),
  },
  repo: actorStore.repo,
})

// Apply the write
await actorStore.applyWrites([write])

Sequencer

The sequencer handles event streaming:
import { Sequencer } from '@atproto/pds/sequencer'

// Access from context
const sequencer = pds.ctx.sequencer

// Start sequencing
await sequencer.start()

// Sequence an event
await sequencer.sequenceCommit(
  did,
  commitData,
  ops
)

Authentication

The PDS includes OAuth provider and session management:
// Access account manager
const accountManager = pds.ctx.accountManager

// Create account
const account = await accountManager.createAccount({
  email: 'user@example.com',
  handle: 'user.pds.example.com',
  password: 'secure-password',
  inviteCode: 'optional-invite',
})

// Verify email
await accountManager.verifyEmail({
  did: account.did,
  token: emailToken,
})

Lexicon Server

Create and configure the XRPC server:
import { createLexiconServer } from '@atproto/pds'

const server = createLexiconServer({
  validateResponse: false,
  payload: {
    jsonLimit: 150 * 1024,
    textLimit: 100 * 1024,
    blobLimit: 5 * 1024 * 1024,
  },
  rateLimits: {
    creator: (opts) => new MemoryRateLimiter(opts),
    global: [
      {
        name: 'global-ip',
        durationMs: 5 * 60 * 1000,
        points: 3000,
      },
    ],
  },
})

Resources

Build docs developers (and LLMs) love