← Back to Guide Index

= Bluesky Personal Data Server (PDS): Identity and Data Hosting

== What is a PDS?

A Personal Data Server is the foundational unit of the AT Protocol network. It serves as a user's home base in the decentralized social web. When you create a Bluesky account on your PDS, you're not creating an account on Bluesky's servers—you're creating it on your own infrastructure, and Bluesky (and other AppViews) access your data from there.

Conceptually, a PDS performs several critical functions:

Identity Management: Your PDS hosts your DID (Decentralized Identifier) document, which is the root of your identity in the AT Protocol ecosystem. This document contains your public keys and links to your handle. When other services want to verify you are who you say you are, they resolve your DID through your PDS.

Repository Hosting: Your "repo" is a cryptographically signed log of all your data—posts, likes, follows, profile changes, everything. It's stored as a Merkle tree, which means any change creates a new root hash that depends on all previous changes. This provides tamper evidence: if someone modifies your data, the signatures won't match.

Authentication Services: Your PDS issues and validates JWTs (JSON Web Tokens) that prove identity. When you log into a client app, your PDS validates your credentials and issues a token. When that app makes requests to other services, it presents this token for authentication.

Blob Storage: Media files (images, videos) are stored separately from the repository because they're large and don't need the same cryptographic verification. Your PDS stores these blobs and serves them on request.

Firehose Participation: Your PDS connects to the network relay and publishes a stream of all changes to your repository. This is how AppViews (like Bluesky's app) stay up-to-date with your data.

== The Architecture of Federation

Understanding how your PDS fits into the broader network helps clarify why certain configuration choices matter.

When you post on Bluesky, here's what happens:

1. You write a post in a client app

2. The client sends it to your PDS with your authentication token

3. Your PDS validates the token, validates the data structure, and adds it to your repository

4. Your PDS signs the new repository state with your private key

5. Your PDS publishes this change to the relay via WebSocket

6. The relay broadcasts it to all connected AppViews

7. AppViews (including Bluesky) receive the update and display your post

The key insight: your PDS is the source of truth for your data. Bluesky's AppView is just one consumer of that data. Other AppViews could display the same data differently, or focus on different subsets.

== DID Documents and Web Resolution

Your PDS uses a did:web identifier: did:web:pds.snek.cc. This is one of several DID methods in the AT Protocol. The did:web method works like this:

The DID itself encodes the URL where the DID document can be found. For did:web:pds.snek.cc, any service can resolve this by:

1. Extracting the domain: pds.snek.cc

2. Making an HTTPS request to https://pds.snek.cc/.well-known/did.json

3. The response is your DID document—a JSON structure containing your public keys and service endpoints

Why this matters for configuration: Your PDS must be accessible at https://pds.snek.cc and must serve the DID document at the correct path. If Caddy isn't proxying correctly, or if the PDS isn't configured with the right hostname, resolution fails and the entire identity system breaks.

== Repository Structure and Merkle Trees

Your repository is stored in files under /var/lib/pds. The core files are:

account.sqlite: Contains account metadata, session information, and handle mappings.

seq.sqlite: Tracks sequence numbers. Every change to your repo increments a sequence number, ensuring strict ordering and preventing replay attacks.

blocks/: This directory contains the actual repository data in a content-addressed format. Each "block" is a chunk of data identified by its hash. The Merkle tree structure means the root hash depends on all leaf hashes, creating a chain of cryptographic proof.

When your PDS starts, it checks these files for consistency. If they're corrupted, the PDS may refuse to start (to prevent serving invalid data). This is why backing up /var/lib/pds is critical—you can't just "recreate" a PDS with the same DID, because you'd have different cryptographic keys and a different repository history.

== The Role of PLC (DID Placeholder)

AT Protocol uses a separate service called the DID Placeholder (PLC) for handle resolution and DID registry. When you create an account, your PDS registers your DID with PLC.

The PDSDIDPLC_URL setting points to this service (https://plc.directory). If this service is unreachable, new accounts can't be created, but existing accounts continue to work because their DIDs are already registered.

== Firehose and WebSocket Connections

Your PDS maintains a persistent WebSocket connection to the relay (the "firehose"). This connection:

The PDS can connect to multiple relays for redundancy. The PDS_CRAWLERS setting lists which relays to connect to. These crawlers consume your firehose to index your data.

Network considerations: This WebSocket connection is long-lived. If it drops, your data stops propagating to the network. The PDS should automatically reconnect, but firewall rules, rate limits, or network issues can cause problems. Your TCP keepalive settings (in the kernel sysctl configuration) help detect dead connections faster.

== Authentication Flow

Understanding authentication helps clarify the role of secrets:

1. Account Creation: When you create an account, the PDS generates cryptographic keypairs. The private keys are stored in the database, protected by the admin password. The PLC rotation key is particularly important—it's used to update your DID document if you need to rotate keys.

2. Login: When you log in with handle and password, the PDS validates against the stored password hash and issues a JWT.

3. Session: The JWT is signed with the JWT secret (configured via PDSJWTSECRET). This is a symmetric key—both the PDS (to sign) and any service validating tokens (to verify) need this secret.

4. Requests: Clients include the JWT in requests. The PDS validates the signature and expiration, then processes the request.

Security implications: Anyone with your JWT secret can forge authentication tokens for any user on your PDS. Anyone with your PLC rotation key can modify your DID document, potentially taking over your identity. These must be kept secret.

== The Invite System

Setting PDSINVITEREQUIRED = "true" means new accounts need an invite code. This is controlled through the admin API.

Why use invites?

How it works: You (the admin) generate invite codes via the admin API. Each code can typically be used a set number of times. When someone signs up, they provide the invite code, which is validated against the database.

Your pds-admin.sh script provides a convenient interface to this API for creating invites and managing accounts.

== Email Integration

The PDS can send emails for password resets and notifications. This requires SMTP credentials configured through environment variables.

The SMTP URL format includes the protocol (smtps for SSL), username, password, server, and port. These credentials are sensitive and should be in sops, not plain text in your configuration.

If email isn't configured, password resets don't work. Users would need manual intervention to regain access to locked accounts.

== Data Storage Configuration

Several settings control where data lives:

PDSDATADIRECTORY: The root directory for all PDS data. Must be persistent and have sufficient space for growth.

PDSBLOBSTOREDISK_LOCATION: Where media files are stored. Large binary data can consume significant space. Monitoring disk usage is important.

PDSBLOBUPLOAD_LIMIT: Maximum file size for uploads. Setting this too high increases storage costs and attack surface. Setting it too low prevents legitimate use.

== On-Demand TLS Validation

Recall from the Caddy section that on-demand TLS asks your PDS to validate subdomains. The endpoint is /tls-check.

When Caddy receives a request for user.pds.snek.cc, it makes a request to https://pds.snek.cc/tls-check?domain=user.pds.snek.cc. Your PDS looks up whether that user exists. If yes, it returns 200 OK. If no, it returns an error, and Caddy rejects the connection.

This prevents certificate spam—Caddy won't request certificates for random subdomains that don't exist.

Configuration dependency: This requires Caddy to be able to reach your PDS. If the PDS is down or misconfigured, new subdomains can't get certificates, but existing certificates continue to work until expiry.

== Relationship with AppViews

Your PDS provides data, but AppViews provide the interface. The Bluesky AppView is the primary one, but others exist or could be built.

AppViews are configured in your PDS settings:

When you browse Bluesky, you're not talking directly to PDS instances—you're talking to the AppView, which aggregates data from all PDS instances in the network.

The PDS also supports moderation services (PDSREPORTSERVICE_URL) for content reporting and enforcement.

== Monitoring and Health

Your PDS exposes health endpoints that monitoring systems can check:

Keeping an eye on these helps detect problems early—disk full, database corruption, network issues, etc.

== Backup Strategy

Your PDS data is irreplaceable. If you lose it:

What to back up:

Backup considerations:

== Scaling Considerations

As your PDS grows:

Disk space: Monitor /var/lib/pds/blocks. Media files accumulate quickly. You may need to implement retention policies or move to object storage.

Database: SQLite works for small PDS instances but may bottleneck at scale. The PDS can use PostgreSQL instead for better performance.

Network bandwidth: The firehose sends all your users' data. More users = more bandwidth. The relay connection can become a bottleneck.

Rate limits: Your PDS should enforce rate limits to prevent abuse. The PDSRATELIMIT_ENABLED setting turns on basic protection.

== Common Issues and Debugging

"DID resolution failed": Usually means the DID document isn't accessible. Check Caddy configuration, SSL certificates, and that the PDS is serving the .well-known path correctly.

"JWT verification failed": The JWT secret may have changed (if you rotated it without updating all services), or tokens may be expired.

"Repository validation failed": Corruption in the SQLite databases. May require restore from backup.

"Can't connect to relay": Network issue, firewall blocking WebSocket, or relay is down. Check connectivity and logs.

"Certificate errors": Clock skew (system time wrong), or Caddy can't reach the PDS for validation. Check time sync and network paths.

== Integration with NixOS

The NixOS PDS module handles:

Important: The module generates a systemd service that:

When you change PDS configuration and rebuild, NixOS:

1. Updates the systemd service definition

2. If the service changed, systemd reloads it

3. The PDS may restart (depending on what changed)

Because the PDS data directory persists, restarts don't lose data—they just reload configuration.

== References