The AT Protocol (atproto) is the open protocol under Bluesky. Your account is a permanent DID, your handle is a DNS name, and your posts, likes and follows live as signed records in a repository on a Personal Data Server (PDS). Self-hosting a PDS means running that server yourself, with the official container, a domain, TLS and SMTP.
This guide sits next to our explainer on end-to-end encrypted messaging, the Double Ratchet, MLS and metadata, our post on whether it is safe to self-host apps on the internet, and our guide to private email on a custom domain, which matters because a PDS sends verification and identity mail. Every protocol fact below comes from the specifications and guides on atproto.com, the bluesky-social repositories on GitHub and Bluesky's own pages, checked on 24 October 2026. The did:plc details also use the method's own specification, which Bluesky originally wrote.
Key takeaways
- AT Protocol is a federated protocol for public social apps. Its core services are Personal Data Servers (which host accounts), Relays (which merge every PDS into one firehose) and AppViews (which index the network for an app such as Bluesky). Feed generators and labelers are supporting services.
- An account is a permanent DID (
did:plcordid:web). A handle is a DNS hostname that must link both ways to the DID, through a_atprotoTXT record or an HTTPS well-known file. - Public data lives in a per-account repository: a Merkle Search Tree of records with a signed commit on top, exported as a single CAR file.
- The official PDS is a Docker Compose stack (PDS, Caddy for automatic TLS, an auto-updater) installed by a script on a fresh Ubuntu or Debian server. Bluesky recommends 1 GB RAM, one core and 20 GB SSD for 1 to 20 users.
- With did:plc, a rotation key you hold yourself is what lets you leave a host that has vanished. A higher-priority rotation key can override a lower-priority one for 72 hours.
- Posts, profile, likes, follows and blocks are public by design. Bluesky says its direct messages are not end-to-end encrypted.
- The self-hoster's security work is custody and hygiene: protect
/pds/pds.env, keep invite codes and rate limits on, back up the whole data directory, and keep the image updated.
What is the AT Protocol?
The atproto specification calls the Authenticated Transfer Protocol "a network protocol for building open social web applications." Its stated design themes are self-authenticating data and identity (so accounts can migrate and content can be redistributed), a design for "big world" scale of billions of accounts, delegated authority over app schemas and aggregation, and reuse of content-addressed data models from peer-to-peer systems on top of ordinary web networking.
The protocol overview is explicit that atproto is federated, not peer to peer: account data sits on host servers and is synchronised over HTTP and WebSockets. The sync spec explains the name. Any server can pass your posts along, and any reader can check your signature on them without asking your server.
The spec also lists two gaps. Non-public data (personal and group sharing) is planned, and the spec recommends against "bolting on" encryption with today's primitives. Formal governance is planned too: the intent is to take the specifications to a standards body such as the IETF once several organisations interoperate. Today atproto is a protocol for public conversation, specified and largely implemented by Bluesky Social PBC.
Bluesky itself is one application on atproto. It uses the app.bsky.* schemas for posts, likes, follows and profiles. Other apps define schemas under their own namespaces and store records in the same repositories. The glossary calls the whole ecosystem the "Atmosphere."
What are the parts of the network?
The protocol overview names three core services and two supporting ones. The PDS README lists every one of them as open to federation.
| Component | What it does | Who usually runs it | Resource profile |
|---|---|---|---|
| Personal Data Server (PDS) | Hosts accounts, stores repositories and blobs, holds signing keys, handles login and OAuth, proxies client requests, emits a firehose for its own accounts | Bluesky for most users; self-hosters and independent providers | Small: 1 GB RAM, 1 core, 20 GB SSD for 1 to 20 users |
| Relay | Subscribes to many PDS firehoses and outputs one full-network stream | Bluesky runs bsky.network; others exist | Bandwidth heavy; since Sync 1.1 not required to keep archives |
| AppView | Indexes the network for one app: counts, threads, search, follower lists | Bluesky runs the one behind the Bluesky app | Resource intensive |
| Feed generator | Returns post URIs for a custom algorithm | Anyone | Depends on the algorithm |
| Labeler | Publishes signed labels about accounts and records | Bluesky's moderation service and independent moderators | Light to moderate |
Personal Data Server
The glossary says a PDS "will always store the user's data repo and signing keys," and may also assign the handle and the DID. It does not usually run apps itself, though it serves account screens such as the OAuth login page. The overview calls it "your home in the cloud": it hosts and distributes your data, manages your identity and makes requests to other services for you.
Relay and the firehose
The sync spec describes the firehose as a WebSocket event stream carrying #commit and #sync events for repository changes, #identity events for DID and handle changes, and #account events for hosting status. Every PDS exposes its own at com.atproto.sync.subscribeRepos. A Relay subscribes to many and republishes one stream, with sequence numbers so consumers can resume, and a hard 5 MB limit per frame. The glossary notes Relays are an optimisation: a small AppView could read PDS hosts directly. The self-hosting guide adds that since Sync 1.1, Relays need not keep archival copies, so they are bandwidth intensive rather than storage intensive.
AppView
An AppView works much like a web search engine, in the glossary's comparison: it aggregates data from across the network to build its views, and writes back to users' repositories through their PDS once they log in with OAuth. A PDS points at Bluesky's AppView by default through PDS_BSKY_APP_VIEW_URL (https://api.bsky.app).
Feed generators
The bluesky-social feed-generator README describes the flow: your PDS asks the generator, found through its DID document, for a "skeleton" of post URIs, authenticated with a JWT signed by your repo signing key, then hydrates those URIs into full posts. Each algorithm is declared as a record in its creator's repository. The starter kit uses did:web and suggests did:plc for a long-lived generator that may change domains.
Labelers
The label spec defines labels as "a form of metadata about any account or content": a source DID, a subject URI and a short value. Labels are signed and distributed outside repositories. Their first purpose was composable moderation, where independent services publish labels and clients choose which to honour. A PDS sends user reports to PDS_REPORT_SERVICE_URL, which defaults to Bluesky's mod.bsky.app.
How does identity work in atproto?
DIDs: the permanent identifier
The W3C published Decentralized Identifiers v1.0 as a Recommendation on 19 July 2022. atproto supports only two "blessed" DID methods:
- did:plc, a self-authenticating method built for atproto, for example
did:plc:ewvi7nxzyoun6zhxrhs64oiz. - did:web, based on HTTPS and DNS, hostname-level only. The atproto DID spec warns that it "does not provide a mechanism for migration or recovering from loss of control of the domain name."
From a resolved DID document, software reads three things: the handle claim (an at:// URI in alsoKnownAs), the atproto signing key (a Multikey entry in verificationMethod with id ending #atproto), and the PDS location (a service entry with id ending #atproto_pds and type AtprotoPersonalDataServer). That last field is the core of portability. The DID never changes; the document says which server hosts you today.
did:plc and the PLC directory
PLC stands for "Public Ledger of Credentials." The did-method-plc README says Bluesky Social PBC developed it in 2022, that more than 40 million atproto identifiers have been registered, and that a PLC Organization was founded in 2026 as a neutral steward committed to backward-compatible changes. The PLC specification describes the mechanics:
- Each identity starts from a signed genesis operation, and the DID string is derived from its hash.
- Control rests in rotation keys: a priority-ordered list of one to five public keys, k256 or p256 only. They live in the operation log, not the DID document.
- Each later operation references the previous one by hash and is signed by a current rotation key. The directory at
plc.directoryvalidates operations and publishes an audit log per DID. - A 72 hour recovery window lets a higher-priority key (lower index) nullify operations signed by a lower-priority key.
- A tombstone operation permanently deactivates a DID, subject to the same window.
- Every operation for every DID, including nullified ones, can be exported and audited, as a check on the directory itself.
The spec also lists privacy effects. Audit log timestamps make an identity's creation date public, and reusing a rotation or signing key across accounts publicly links them.
Signing keys versus rotation keys
The overview draws the line. The signing key validates the repository and is entrusted to the PDS so it can write records for you. Rotation keys change the DID document itself, and "can be controlled by the user, e.g. as a paper key," so a user can move to a new PDS without the old host's help. did:web has no rotation keys. The PLC spec calls separating the two a best practice.
Cryptography in one paragraph
The cryptography spec requires support for two curves, p256 and k256; the reference implementation generates k256 by default. Signed objects are encoded in a strict CBOR profile (DRISL), hashed with SHA-256 and signed, giving 64-byte signatures. Signatures over CBOR must use the "low-S" form, which closes off ECDSA malleability. Data encryption "is not used directly in the protocol"; transport security comes from TLS.
Handles: DNS names that point back to the DID
A handle such as alice.example.org must link both ways: the DID document claims it, and the handle resolves to the DID. Without the second half, anyone could claim to be anyone.
| Method | Where it lives | Format | Best for |
|---|---|---|---|
| DNS TXT | _atproto.<handle> | did=<DID>; exactly one valid record | Individuals; the recommended method |
| HTTPS well-known | https://<handle>/.well-known/atproto-did | Plain text body with only the DID, HTTPS on 443 | Large services that cannot automate TXT records |
Rules from the handle spec that catch people out: handles are lowercase and need at least two labels; .alt, .arpa, .example, .internal, .invalid, .local, .localhost and .onion must fail resolution; keep handles to 244 characters because _atproto. adds nine; DNSSEC is not required; if the two methods disagree, prefer DNS. A handle that stops resolving is shown as handle.invalid, and the PDS may block writes until it verifies again. The spec also flags lookalike handles as an impersonation risk and suggests reserving names such as www, admin and mail if you hand out subdomains.
How are repositories stored and signed?
Records, collections and Lexicon
Each account has one repository of public records, keyed by paths of the form <collection>/<record-key>. The collection is a Namespaced Identifier (NSID) in reverse-DNS form, such as app.bsky.feed.post; the record key is often a TID, a timestamp-based ID that sorts chronologically. Lexicon, which the glossary compares to JSON Schema and OpenAPI, defines record types, HTTP endpoints (XRPC) and event stream messages. Because NSIDs are reverse-DNS, schema governance follows domain ownership.
The Merkle Search Tree
Records sit in a Merkle Search Tree (MST), a structure from a 2019 paper by Auvolat and Taïani. The repository spec specifies that the tree is deterministic, so the same records always give the same root hash; each key's depth comes from counting leading zero bits of its SHA-256 hash in 2-bit chunks, a fanout of 4; and records, nodes and commits are DRISL CBOR objects linked by content identifier (CID). Media "blobs" are referenced by CID but stored outside the repository. Repositories are meant for up to single-digit millions of records.
The signed commit
The commit has did, version (3 for the current format), data (the MST root CID), rev (a TID acting as a logical clock that must always increase), prev (now virtually always null) and sig. The PDS serialises the unsigned commit, hashes it with SHA-256 and signs it with the current signing key. The signature does not name its key, so verifiers fetch it from the DID document, which is why a new commit should follow every signing key rotation. Records can be deleted "without leaving a trace or tombstone," a property of the repository, not of copies others already hold.
CAR files
The export format is CAR v1 (.car, application/vnd.ipld.car): a header naming the commit CID, then every MST node and record as blocks. Anyone can fetch your full export with the unauthenticated com.atproto.sync.getRepo endpoint. Blobs are not inside it; they are listed with com.atproto.sync.listBlobs and fetched one by one. A backup that keeps only the CAR file loses your images and video.
What does running your own PDS involve?
What the official distribution contains
The bluesky-social/pds repository ships images, an installer and docs. The Compose file it writes runs three containers on host networking:
| Container | Image | Job |
|---|---|---|
caddy | caddy:2 | TLS with on-demand certificates for the PDS hostname and every handle subdomain |
pds | ghcr.io/bluesky-social/pds:0.4 | The PDS, reading /pds/pds.env and storing data under /pds |
watchtower | ghcr.io/nicholas-fedor/watchtower:latest | Nightly image updates with cleanup; mounts the Docker socket |
The newest repository tag when we checked was v0.4.5049. The image bundles goat, Bluesky's atproto command line tool. The PDS code lives in the TypeScript bluesky-social/atproto monorepo, and the distribution is dual-licensed MIT and Apache 2.0.
Server, domain and DNS requirements
The README requires a public IPv4 address, a public DNS name, and inbound TCP 80 (TLS verification only) and 443 (all application traffic). It recommends Ubuntu 24.04, 1 GB RAM, one core and 20 GB SSD on amd64 or arm64 for 1 to 20 users. The installer supports Ubuntu 22.04, 24.04 and 26.04 and Debian 11, 12 and 13, on a fresh server with nothing else on ports 80 or 443. DNS needs an A record for the hostname and a wildcard A record for handle subdomains. The README also advises restricting SSH to your own IP. The installer refuses to run if /pds/account.sqlite exists, which protects an existing install.
What the installer generates
The script is short and worth reading before you run it. It installs Docker from Docker's apt repository, creates /pds with mode 700, writes a Caddyfile that asks the PDS at localhost:3000/tls-check before issuing each on-demand certificate, and writes /pds/pds.env with PDS_HOSTNAME, a random PDS_JWT_SECRET, a random PDS_ADMIN_PASSWORD, a freshly generated secp256k1 PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX, a 300 MB blob upload limit, and network defaults (plc.directory, Bluesky's AppView and report service, and bsky.network as the crawler to notify). That file is the crown jewels: its rotation key can sign PLC operations for every account the PDS created.
A PDS sends mail to verify addresses and to deliver the token required for identity changes. The README configures it with PDS_EMAIL_SMTP_URL and PDS_EMAIL_FROM_ADDRESS, through an email service, your own mail server, or a local sendmail-compatible transport. Special characters in credentials must be percent-encoded. If SMTP fails during a migration, the README says the confirmation code can be read from the email_token table in accounts.sqlite.
Storage
The production guide says the official install uses SQLite: one database per user under /pds/actors/ plus a few PDS-wide ones, with blobs on local disk at /pds/blocks. For larger deployments Bluesky "strongly" recommends S3-compatible object storage for blobs and continuous SQLite replication, with PDS_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT=true so the replication tool owns checkpointing.
Joining the network
There is no registration step. The PDS notifies the crawlers in PDS_CRAWLERS and the Relay starts reading its firehose. Two checks confirm you are visible: /xrpc/_health returns a version string, and a WebSocket client can connect to subscribeRepos. The README warns that most failures come from porting the Caddy config to other reverse proxies, where TLS, WebSockets and virtual hosts go wrong, and that Bluesky does not support those setups. The production guide adds that Relays limit signups on new PDS hosts until they build a track record.
How does account migration work, and what if a host disappears?
The protocol view
The account hosting spec reduces migration to a state: if the DID document points at a new PDS, the account is active there, and that PDS holds a validly signed repository with a higher rev, the account has moved.
The cooperative migration
The atproto.com migration guide describes the case where both servers help:
- Create a deactivated account on the new PDS, proving control with a service auth token from the old PDS (
com.atproto.server.getServiceAuth). - Move the repository: export a CAR from the old PDS and import it with
com.atproto.repo.importRepo. - Move blobs one by one after the repo is indexed. The reference PDS limits blob uploads to 1,000 per 24 hours per IP address.
- Move private preferences (
app.bsky.actor.getPreferencesandputPreferencesfor Bluesky). - Update the identity: request a PLC operation signature from the old PDS (a short-lived token goes to your verified email), have it sign with its rotation key, and submit through the new PDS, which checks the result will not break the account.
- Activate on the new PDS and deactivate on the old one.
Some services cache identity aggressively, so service auth can fail for up to 24 hours. The old PDS can help with PLC recovery during a fixed 72 hour window, but only if the old account was not fully deleted. The goat CLI and community tools automate these steps.
The adversarial case
If the old PDS is offline or hostile, the guide calls it an "adversarial migration," and the outcome depends on what you hold:
- did:plc with your own rotation key: sign a PLC operation yourself that sets a new signing key and PDS, then restore from backup. This is the scenario the overview describes, where a PDS "disappears without notice."
- did:plc where only the old PDS holds a rotation key: you depend on that operator. The account spec says users should "take special care" choosing a host they delegate identity management to.
- did:web: you control the DID as long as you control the domain, and lose it with the domain.
Without a backup, the account keeps its identity but old content is missing, unless a mirror still has it.
Moving the PDS itself
Do not wipe and reinstall a PDS on the same hostname. The README warns that this, or a hostname change without cutover, desyncs the PDS from the Relay; migrate accounts one at a time instead. If you do desync, the README documents a fix: read the Relay's last sequence number with goat relay host status, stop the PDS, set repo_seq in sequencer.sqlite one higher, and restart. Changing a PDS's domain needs a PLC update for every account, which the production guide calls possible but "not turnkey."
What is public by design, and what is not?
Bluesky's privacy policy says most activity is "public by design," naming posts, profile, likes, following and blocks. That follows from the protocol: these are repository records, repository contents are "entirely public and verifiable," anyone can download your repository without authentication, and every Relay subscriber sees your writes live. Your DID document is public too, and for did:plc so is the full operation history.
What stays private:
- Preferences such as muted words and feed settings, fetched with authenticated endpoints.
- Email and password data on the PDS. The production guide notes a moderation service can be granted access to account email, and advises separate moderation instances for a PDS and an AppView for that reason.
- Direct messages are not repository records. Bluesky's privacy policy says DMs are stored and processed by Bluesky, are "unencrypted," and "can be accessed for Trust & Safety purposes." Self-hosting does not move them.
Deletion has limits. The sync spec asks mirrors to honour deletions "within seconds or minutes" and not to redistribute bulk snapshots, but a consumer that recorded your post first still has it.
Self-hosting moves custody of your database, blobs, email address and signing key to you. It does not make public records private, and it adds new metadata on your own server: logs with the IP addresses of clients and services. Container logs go to Docker's log by default and are lost on reboot unless you set LOG_DESTINATION. Choose on purpose.
What are the security issues for a self-hosted PDS?
Rotation key custody
There are two kinds of PLC rotation key to think about:
- The PDS's own key in
pds.env, used for PLC operations on hosted accounts, including recovery. Anyone who readspds.envcan rewrite every hosted identity. The production guide recommends holding it in a key management system or hardware security module. - A recovery key you keep offline. The production guide describes
PDS_RECOVERY_DID_KEY, a public key the PDS adds to users' DIDs, whose private half is stored safely for recovery after losing the PDS key. For individuals, the migration guide recommends adding a self-controlled rotation key if you can manage a private key securely.
Because a higher-priority key can nullify a lower-priority one for 72 hours, order matters. A key you hold offline above the PDS's key is your safety net if the server is compromised. Check your DID's history (goat plc history <handle>) to spot unexpected operations.
Admin password, invite codes and rate limits
PDS_ADMIN_PASSWORD authorises account creation and deletion, takedowns, password resets and invites. Treat it as a root password, and run admin commands inside the container where it is already set rather than typing it into shell history.
PDS_INVITE_REQUIRED defaults to true, and the production guide says plainly: "Don't disable invite codes." It calls them one of the most effective controls against automated signups. An open PDS that fills with spam can get the whole host limited at the Relay.
PDS_RATE_LIMITS_ENABLED also defaults to true; removing limits can get the PDS limited at the Relay. The migration guide documents PDS_RATE_LIMIT_BYPASS_IPS and a bypass header for trusted imports, and warns that the bypass covers all rate limits. Remove it when the import finishes.
Domain separation
The production guide warns against a PDS on a subdomain of a domain that also runs another web app: user blobs are served from the PDS domain, which is also where users enter passwords and complete OAuth, which invites credential theft. Use a separate registered domain. The PDS sends restrictive CORS headers by default.
Backups and updates
The installer prints "Backup service data: /pds." That directory holds the SQLite databases, local blobs, the Caddy certificate store and pds.env, so a backup also contains the admin password and rotation key. Encrypt it. User-level CAR and blob exports are a second line of defence that works even if the server is gone.
The Compose file runs Watchtower nightly with /var/run/docker.sock mounted, which effectively gives it root on the host. pdsadmin update is the manual alternative: it downloads the current compose.yaml from the repository's main branch, pulls the image and restarts. pdsadmin itself downloads each subcommand script from GitHub at run time. Both trust the repository and the registry. You can drop the auto-updater and update on your own schedule, but then you own the patch cadence, and an unpatched public login server is the worse outcome.
Threats to plan for
| Threat | What it allows | Main control |
|---|---|---|
pds.env leak | Rewrite hosted DIDs, run admin actions | Strict permissions, encrypted backups, KMS or HSM, offline higher-priority recovery key |
| Admin password leak | Create, delete or take down accounts | Unique random password kept out of shell history |
| Open signups | Spam, Relay limits on your host | Invite codes on, captcha, a labeler |
| Unpatched image | Known vulnerabilities stay exposed | Auto-updates or scheduled pdsadmin update, plus image scanning |
| DNS or registrar takeover | Handle hijack, did:web loss | Registrar lock, MFA on the DNS account, watch the _atproto record |
| Lost server, no backup | Missing content; possible identity loss if only the PDS held a rotation key | Encrypted /pds backups, CAR and blob exports, self-held rotation key |
| Lookalike handles | Impersonation | Reserve sensitive subdomains; verify handles both ways |
How does atproto compare with ActivityPub and Matrix?
ActivityPub became a W3C Recommendation on 23 January 2018 and describes itself as "a decentralized social networking protocol" with client-to-server and server-to-server APIs. Matrix describes open APIs for decentralised communication over a federation of servers. The table compares design choices, not quality.
| AT Protocol | ActivityPub | Matrix | |
|---|---|---|---|
| Primary use | Public social apps | Federated social networking | Real-time messaging and bridges |
| Identity | Permanent DID plus a DNS handle | Actor identified by a URL on its server | User ID includes the homeserver's domain |
| Where data lives | Signed repository on the user's PDS, mirrored by Relays and AppViews | On the actor's server, delivered to followers' inboxes | Room history replicated on every participating homeserver |
| Integrity | Every commit signed with the account key | Not mandated by the Recommendation | Events signed by the sending homeserver |
| Federation | Hosts separate from aggregators; apps see a global view | Server to server delivery; each server sees what it receives | Servers in a room exchange events |
| Moving servers | Built in via DID update plus CAR and blob import | Not defined in the Recommendation | Not a protocol feature |
| Moderation | PDS rules, AppView policy, user-chosen labelers | Per server, plus a Block activity | Per server and room, server ACLs, power levels |
| Private messages | Not in the protocol yet | Addressed activities, no end-to-end encryption in the spec | End-to-end encryption with Olm and Megolm |
Bluesky's 22 February 2024 announcement of open federation states the core difference: on Bluesky, server choice does not change what content you see, and you can change servers without losing your username, follows or posts.
| Your goal | Fits best |
|---|---|
| Own your public social identity and data, keep using the Bluesky app | Self-hosted PDS |
| A community whose server sets its culture and rules | A federated ActivityPub server |
| Private group chat with end-to-end encryption | A self-hosted Matrix homeserver |
| Moderate for others without hosting accounts | An atproto labeler |
Step by step: install a PDS, set a DNS handle, create an account, back up, update
These steps follow the official README. Replace pds.example.net with your hostname and 203.0.113.10 with your server's IP.
1. Prepare DNS
pds.example.net A 203.0.113.10 600
*.pds.example.net A 203.0.113.10 600
dig +short A pds.example.net
dig +short A random123.pds.example.net
Both must print the server's IP. An IPv6-only host will not work; the README requires public IPv4.
2. Open the firewall
In your cloud firewall, allow TCP 80 and 443 from anywhere and TCP 22 only from your IP. The host firewall equivalent:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow from 198.51.100.7 to any port 22 proto tcp
sudo ufw enable
Use your own IP in place of 198.51.100.7, and confirm SSH works in a second session before closing the first.
3. Download, read, then run the installer
curl -fsSL https://raw.githubusercontent.com/bluesky-social/pds/main/installer.sh -o installer.sh
less installer.sh
sudo bash installer.sh
sudo ls -ld /pds
sudo ls -l /pds/pds.env
The script asks for the hostname, an admin email and, optionally, a first account. Save the printed password; it is not shown again. /pds should be mode 700 and owned by root.
4. Check health and the firehose
curl -s https://pds.example.net/xrpc/_health
wsdump "wss://pds.example.net/xrpc/com.atproto.sync.subscribeRepos?cursor=0"
The health call returns a JSON version. The WebSocket stays quiet until there are events; a TLS error or refused connection means the Relay cannot read you either.
5. Configure SMTP
sudo nano /pds/pds.env
# PDS_EMAIL_SMTP_URL=smtps://USERNAME:PASSWORD@smtp.example.org:465/
# PDS_EMAIL_FROM_ADDRESS=pds@example.net
sudo systemctl restart pds
Percent-encode special characters in credentials. Our custom domain email guide covers SPF, DKIM and DMARC so verification mail arrives.
6. Create an account
sudo pdsadmin account create alice@example.org alice.pds.example.net
sudo pdsadmin account list
sudo pdsadmin create-invite-code
pdsadmin account create creates a single-use invite code with the admin password, registers the account with it, and prints the DID and a generated password. That is the programmatic route the production guide recommends instead of switching invites off. Hand out codes from create-invite-code only to people you intend to host.
7. Use your own domain as your handle
To use alice.example.org, publish the account's DID in a TXT record:
_atproto.alice.example.org TXT "did=did:plc:abcd1234exampleexample"
dig +short TXT _atproto.alice.example.org
curl -s "https://pds.example.net/xrpc/com.atproto.identity.resolveHandle?handle=alice.example.org"
Then change the handle from a client signed in to that account, such as the Bluesky app with your PDS URL entered as the hosting provider. The client update writes the claim into the DID document; the TXT record proves the other direction. Keep exactly one did= record on that name.
8. Back up the server and each repository
Stop the service so the SQLite files are consistent:
sudo systemctl stop pds
sudo tar -czf /root/pds-backup-$(date +%F).tar.gz -C / pds
sudo systemctl start pds
sudo tar -tzf /root/pds-backup-$(date +%F).tar.gz | head
The archive contains pds.env, so encrypt it (for example with age or gpg) before it leaves the machine. Then take account-level copies from any machine with goat installed (brew install goat or go install github.com/bluesky-social/goat@latest):
goat repo export alice.example.org
goat blob export alice.example.org
goat plc history alice.example.org > alice-plc-history.json
That gives a timestamped CAR file, a folder of blobs named by CID, and your identity's operation history: what a new PDS needs if this one is gone. Larger servers should follow the production guide's continuous replication and object storage advice instead.
9. Test the restore
Unpack the archive on a scratch machine and check the files are there:
mkdir -p /tmp/pds-restore-test
tar -xzf pds-backup-2026-10-24.tar.gz -C /tmp/pds-restore-test
ls /tmp/pds-restore-test/pds
Never start a restored copy on the public hostname while the live PDS runs, and never restore over a live /pds without stopping the service first. Two servers broadcasting for one hostname desync you from the Relay.
10. Update the server
sudo pdsadmin update
curl -s https://pds.example.net/xrpc/_health
sudo docker logs --tail 50 pds
The default install also updates itself nightly. Neither path touches the host operating system, so patch it with your distribution's normal process.
Common mistakes
- Porting the Caddy config to another proxy without testing WebSockets. The README names this as the top cause of a PDS the network cannot see.
- Skipping the wildcard DNS record. Handle subdomains need it for on-demand certificates.
- Reinstalling on the same hostname. It desyncs you from the Relay. Migrate accounts instead.
- Turning off invite codes. The production guide says not to, and spam can get your host limited.
- Unencrypted backups of
pds.env. It holds the rotation key for every hosted account. - Relying on the PDS's rotation key alone. If the server and backups are lost, nobody can sign for you.
- Choosing did:web without a plan for the domain. Lose the domain and you lose the identity.
- Putting the PDS on a subdomain of your app's domain. Blobs and login pages would share an origin with your app.
- Backing up only the CAR file. Blobs are not in it.
- Assuming self-hosting makes posts private. Repository records are public to anyone who asks.
FAQ
Is Bluesky decentralised if most people use Bluesky's servers?
The protocol lets anyone host accounts and run relays, AppViews, feeds and labelers, and the PDS README lists all of them as open to federation. In practice most accounts are hosted by Bluesky, the default AppView, Relay and moderation service in the PDS configuration are Bluesky's, and did:plc depends on one central directory. Self-hosting moves your data and keys, not those services.
How much does it cost to run a Bluesky PDS?
The recommended spec for 1 to 20 users is small: 1 GB RAM, one core, 20 GB SSD, a public IPv4 address and a domain. Add an email sending service and backup storage. Media-heavy accounts need more disk or object storage.
Can I use my own domain as my Bluesky handle without self-hosting?
Yes. Handles are independent of hosting. Publish a _atproto TXT record (or the well-known file) pointing to your DID and change the handle in your client.
What happens to my followers if I move to my own PDS?
Nothing they will notice. Follows are records in their repositories that point at your DID, and your DID does not change.
What happens if my self-hosted PDS goes offline?
Your content stops updating and services may show cached copies; the account spec says status should stay unchanged during temporary outages. If the server is gone for good, a self-held rotation key plus CAR and blob backups let you move to a new PDS.
Is did:plc or did:web better for a self-hoster?
did:plc has rotation keys, the 72 hour recovery window, and survives losing a domain. did:web avoids the PLC directory but ties identity permanently to a domain with no recovery. Accounts created by the official PDS get a did:plc by default.
Are Bluesky direct messages stored on my PDS?
No. Bluesky's privacy policy says DMs are stored and processed by Bluesky, are unencrypted, and can be accessed for trust and safety purposes. Private data is listed as future work in the atproto spec.
How do I keep my PDS updated?
The default install updates nightly through Watchtower. You can also run sudo pdsadmin update, which fetches the current Compose file, pulls the image and restarts. Patch the host operating system separately.
Where Safeguard fits
Safeguard does not host, monitor or moderate atproto services, and it does not hold or protect DID rotation keys, read your firehose or touch your posts. The fit is indirect. A PDS is open source software shipped as a container image with a dependency tree, and that is the part Safeguard can look at: safeguard scan --image ghcr.io/bluesky-social/pds:0.4 lists the components in the image you run and matches them against vulnerability databases, and safeguard scan --dir . does the same for a feed generator or labeler repository you maintain. Results land as JSON under ./output, and the platform shows which projects contain a given component and version. That covers known vulnerabilities in components, not your configuration, DNS or key custody.
A self-hosted PDS is still a stack of open source parts: the TypeScript PDS and its npm dependencies, the runtime in its base image, Caddy, the auto-updater, and any feed generator or labeler you build. The approaches below apply to the pieces you build and ship yourself; the PDS image itself is fixed by Bluesky's releases, and fork and patch is early access.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
These demos show Safeguard working on software projects, not on a social network. They apply to the code and images around a PDS.
The first demo is a real Safeguard CLI session. After install, safeguard opens the interactive terminal, you ask it to fix the lodash CVE in a repository, and it reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. A feed generator built from the TypeScript starter kit is an npm project, so the same loop fits it directly.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with its device code, and then scans from the terminal. For a self-hoster, the scan of your PDS image or labeler code runs on hardware you control.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and shows how you choose which tools Claude can use. If you ask an assistant to help with server configuration, this is how you give it your scan results and nothing more.
Click through it at your own pace, or open it full screen.
Get the apps at safeguard.sh/download and read the CLI docs at docs.safeguard.sh/docs/cli.
Quick check
Why can a did:plc account move to a new PDS even if the old PDS is gone?
Because control of a did:plc identity rests in rotation keys, not in the PDS. A user who holds a rotation key can sign a PLC operation pointing the DID document at a new PDS and signing key, then restore from a CAR and blob backup.
Which file on a self-hosted PDS holds the admin password and the PLC rotation key, and why does that matter for backups?
/pds/pds.env. Anyone who reads it can run admin actions and sign PLC operations for every hosted account, so any backup of /pds must be encrypted and stored as carefully as the server.
Sources
- AT Protocol specification
- AT Protocol: Protocol overview
- AT Protocol: Glossary
- AT Protocol: DID specification
- AT Protocol: Handle specification
- AT Protocol: Repository specification
- AT Protocol: Data synchronization specification
- AT Protocol: Cryptography specification
- AT Protocol: Account hosting specification
- AT Protocol: Labels specification
- AT Protocol: Account migration guide
- AT Protocol: Self-hosting guide
- AT Protocol: Going to production
- bluesky-social/pds README
- bluesky-social/pds installer.sh
- bluesky-social/pds compose.yaml
- bluesky-social/pds pdsadmin update.sh
- bluesky-social/pds pdsadmin account.sh
- bluesky-social/goat README
- bluesky-social/feed-generator README
- did-method-plc README
- DID PLC method specification v0.1
- Bluesky: An Open Social Web (22 February 2024)
- Bluesky Privacy Policy
- W3C Decentralized Identifiers (DIDs) v1.0
- W3C ActivityPub
- Matrix Specification
Last reviewed: 24 October 2026. The PDS distribution changes often; check the bluesky-social/pds README for the version you deploy before applying any step above.