Alternative ATProto PDS implementation
1# ATProto PDS
2```
3 __ __
4 /\ \__ /\ \__
5 __ \ \ ,_\ _____ _ __ ___\ \ ,_\ ___
6 /'__'\ \ \ \/ /\ '__'\/\''__\/ __'\ \ \/ / __'\
7 /\ \L\.\_\ \ \_\ \ \L\ \ \ \//\ \L\ \ \ \_/\ \L\ \
8 \ \__/.\_\\ \__\\ \ ,__/\ \_\\ \____/\ \__\ \____/
9 \/__/\/_/ \/__/ \ \ \/ \/_/ \/___/ \/__/\/___/
10 \ \_\
11 \/_/
12```
13
14This is an implementation of an ATProto PDS, built with [Axum](https://github.com/tokio-rs/axum) and [Atrium](https://github.com/sugyan/atrium).
15This PDS implementation uses a SQLite database to store private account information and file storage to store canonical user data.
16
17Heavily inspired by David Buchanan's [millipds](https://github.com/DavidBuchanan314/millipds).
18This implementation forked from the [azure-rust-app](https://github.com/DrChat/azure-rust-app) starter template and the upstream [DrChat/bluepds](https://github.com/DrChat/bluepds).
19See TODO below for this fork's changes from upstream.
20
21If you want to see this fork in action, there is a live account hosted by this PDS at [@teq.shatteredsky.net](https://bsky.app/profile/teq.shatteredsky.net)!
22
23> [!WARNING]
24> This PDS is undergoing heavy development. Do _NOT_ use this to host your primary account or any important data!
25
26## Quick Start
27```
28cargo run
29```
30
31## Cost breakdown (on Oracle Cloud Infrastructure)
32This is how much it costs to host the @teq.shatteredsky.net account:
33
34- $0/mo [Always Free Resources](https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm)
35 - $0/mo: VM.Standard.A1.Flex
36 - OCPU count: 2
37 - Network bandwidth: 2 Gbps
38 - Memory: 12 GB
39 - $0/mo: Virtual Cloud Network
40 - IPv4 address
41 - IPv6 address
42 - $0/mo: Boot volume
43 - Size: 47 GB
44 - VPUs/GB: 10
45
46This is about half of the 3,000 OCPU hours and 18,000 GB hours available per month for free on the VM.Standard.A1.Flex shape. This is _without_ optimizing for costs. The PDS can likely be made much cheaper.
47
48## Code map
49```
50* migrations/ - SQLite database migrations
51* src/
52 * endpoints/ - ATProto API endpoints
53 * auth.rs - Authentication primitives
54 * config.rs - Application configuration
55 * did.rs - Decentralized Identifier helpers
56 * error.rs - Axum error helpers
57 * firehose.rs - ATProto firehose producer
58 * main.rs - Main entrypoint
59 * metrics.rs - Definitions for telemetry instruments
60 * oauth.rs - OAuth routes
61 * plc.rs - Functionality to access the Public Ledger of Credentials
62 * storage.rs - Helpers to access user repository storage
63```
64
65## To-do
66### Teq's fork
67- [ ] OAuth
68 - [X] `/.well-known/oauth-protected-resource` - Authorization Server Metadata
69 - [X] `/.well-known/oauth-authorization-server`
70 - [X] `/par` - Pushed Authorization Request
71 - [X] `/client-metadata.json` - Client metadata discovery
72 - [X] `/oauth/authorize`
73 - [X] `/oauth/authorize/sign-in`
74 - [X] `/oauth/token`
75 - [ ] Authorization flow - Backend client
76 - [X] Authorization flow - Serverless browser app
77 - [ ] DPoP-Nonce
78 - [ ] Verify JWT signature with JWK
79- [ ] Email verification
80- [ ] 2FA
81- [ ] Admin endpoints
82- [ ] App passwords
83- [X] `listRecords` fixes
84 - [X] Fix collection prefixing (terminate with `/`)
85 - [X] Fix cursor handling (return `cid` instead of `key`)
86- [X] Session management (JWT)
87 - [X] Match token fields to reference implementation
88 - [X] RefreshSession from Bluesky Client
89 - [X] Respond with JSON error message `ExpiredToken`
90- [X] Cursor handling
91 - [X] Implement time-based unix microsecond sequences
92 - [X] Startup with present cursor
93- [X] Respond `RecordNotFound`, required for:
94 - [X] app.bsky.feed.postgate
95 - [X] app.bsky.feed.threadgate
96 - [ ] app.bsky... (profile creation?)
97- [X] Linting
98 - [X] Rustfmt
99 - [X] warnings
100 - [X] deprecated-safe
101 - [X] future-incompatible
102 - [X] keyword-idents
103 - [X] let-underscore
104 - [X] nonstandard-style
105 - [X] refining-impl-trait
106 - [X] rust-2018-idioms
107 - [X] rust-2018/2021/2024-compatibility
108 - [X] ungrouped
109 - [X] Clippy
110 - [X] nursery
111 - [X] correctness
112 - [X] suspicious
113 - [X] complexity
114 - [X] perf
115 - [X] style
116 - [X] pedantic
117 - [X] cargo
118 - [X] ungrouped
119
120### High-level features
121- [ ] Storage backend abstractions
122 - [ ] Azure blob storage backend
123 - [ ] Backblaze b2(?)
124- [ ] Telemetry
125 - [X] [Metrics](https://github.com/metrics-rs/metrics) (counters/gauges/etc)
126 - [X] Exporters for common backends (Prometheus/etc)
127
128### APIs
129- [X] [Service proxying](https://atproto.com/specs/xrpc#service-proxying)
130- [X] UG /xrpc/_health (undocumented, but impl by reference PDS)
131<!-- - [ ] xx /xrpc/app.bsky.notification.registerPush
132- app.bsky.actor
133 - [X] AG /xrpc/app.bsky.actor.getPreferences
134 - [ ] xx /xrpc/app.bsky.actor.getProfile
135 - [ ] xx /xrpc/app.bsky.actor.getProfiles
136 - [X] AP /xrpc/app.bsky.actor.putPreferences
137- app.bsky.feed
138 - [ ] xx /xrpc/app.bsky.feed.getActorLikes
139 - [ ] xx /xrpc/app.bsky.feed.getAuthorFeed
140 - [ ] xx /xrpc/app.bsky.feed.getFeed
141 - [ ] xx /xrpc/app.bsky.feed.getPostThread
142 - [ ] xx /xrpc/app.bsky.feed.getTimeline -->
143- com.atproto.admin
144 - [ ] xx /xrpc/com.atproto.admin.deleteAccount
145 - [ ] xx /xrpc/com.atproto.admin.disableAccountInvites
146 - [ ] xx /xrpc/com.atproto.admin.disableInviteCodes
147 - [ ] xx /xrpc/com.atproto.admin.enableAccountInvites
148 - [ ] xx /xrpc/com.atproto.admin.getAccountInfo
149 - [ ] xx /xrpc/com.atproto.admin.getAccountInfos
150 - [ ] xx /xrpc/com.atproto.admin.getInviteCodes
151 - [ ] xx /xrpc/com.atproto.admin.getSubjectStatus
152 - [ ] xx /xrpc/com.atproto.admin.sendEmail
153 - [ ] xx /xrpc/com.atproto.admin.updateAccountEmail
154 - [ ] xx /xrpc/com.atproto.admin.updateAccountHandle
155 - [ ] xx /xrpc/com.atproto.admin.updateAccountPassword
156 - [ ] xx /xrpc/com.atproto.admin.updateSubjectStatus
157- com.atproto.identity
158 - [ ] xx /xrpc/com.atproto.identity.getRecommendedDidCredentials
159 - [ ] AP /xrpc/com.atproto.identity.requestPlcOperationSignature
160 - [X] UG /xrpc/com.atproto.identity.resolveHandle
161 - [ ] AP /xrpc/com.atproto.identity.signPlcOperation
162 - [ ] xx /xrpc/com.atproto.identity.submitPlcOperation
163 - [X] AP /xrpc/com.atproto.identity.updateHandle
164<!-- - com.atproto.moderation
165 - [ ] xx /xrpc/com.atproto.moderation.createReport -->
166- com.atproto.repo
167 - [X] AP /xrpc/com.atproto.repo.applyWrites
168 - [X] AP /xrpc/com.atproto.repo.createRecord
169 - [X] AP /xrpc/com.atproto.repo.deleteRecord
170 - [X] UG /xrpc/com.atproto.repo.describeRepo
171 - [X] UG /xrpc/com.atproto.repo.getRecord
172 - [ ] xx /xrpc/com.atproto.repo.importRepo
173 - [ ] xx /xrpc/com.atproto.repo.listMissingBlobs
174 - [X] UG /xrpc/com.atproto.repo.listRecords
175 - [X] AP /xrpc/com.atproto.repo.putRecord
176 - [X] AP /xrpc/com.atproto.repo.uploadBlob
177- com.atproto.server
178 - [ ] xx /xrpc/com.atproto.server.activateAccount
179 - [ ] xx /xrpc/com.atproto.server.checkAccountStatus
180 - [ ] xx /xrpc/com.atproto.server.confirmEmail
181 - [X] UP /xrpc/com.atproto.server.createAccount
182 - [ ] xx /xrpc/com.atproto.server.createAppPassword
183 - [X] AP /xrpc/com.atproto.server.createInviteCode
184 - [ ] xx /xrpc/com.atproto.server.createInviteCodes
185 - [X] UP /xrpc/com.atproto.server.createSession
186 - [ ] xx /xrpc/com.atproto.server.deactivateAccount
187 - [ ] xx /xrpc/com.atproto.server.deleteAccount
188 - [ ] xx /xrpc/com.atproto.server.deleteSession
189 - [X] UG /xrpc/com.atproto.server.describeServer
190 - [ ] xx /xrpc/com.atproto.server.getAccountInviteCodes
191 - [X] AG /xrpc/com.atproto.server.getServiceAuth
192 - [X] AG /xrpc/com.atproto.server.getSession
193 - [ ] xx /xrpc/com.atproto.server.listAppPasswords
194 - [ ] xx /xrpc/com.atproto.server.refreshSession
195 - [ ] xx /xrpc/com.atproto.server.requestAccountDelete
196 - [ ] xx /xrpc/com.atproto.server.requestEmailConfirmation
197 - [ ] xx /xrpc/com.atproto.server.requestEmailUpdate
198 - [ ] xx /xrpc/com.atproto.server.requestPasswordReset
199 - [ ] xx /xrpc/com.atproto.server.reserveSigningKey
200 - [ ] xx /xrpc/com.atproto.server.resetPassword
201 - [ ] xx /xrpc/com.atproto.server.revokeAppPassword
202 - [ ] xx /xrpc/com.atproto.server.updateEmail
203- com.atproto.sync
204 - [X] UG /xrpc/com.atproto.sync.getBlob
205 - [X] UG /xrpc/com.atproto.sync.getBlocks
206 - [X] UG /xrpc/com.atproto.sync.getLatestCommit
207 - [X] UG /xrpc/com.atproto.sync.getRecord
208 - [X] UG /xrpc/com.atproto.sync.getRepo
209 - [X] UG /xrpc/com.atproto.sync.getRepoStatus
210 - [X] UG /xrpc/com.atproto.sync.listBlobs
211 - [X] UG /xrpc/com.atproto.sync.listRepos
212 - [X] UG /xrpc/com.atproto.sync.subscribeRepos
213
214## Quick Deployment (Azure CLI)
215```
216az group create --name "webapp" --location southcentralus
217az deployment group create --resource-group "webapp" --template-file .\deployment.bicep --parameters webAppName=testapp
218
219az acr login --name <insert name of ACR resource here>
220docker build -t <ACR>.azurecr.io/testapp:latest .
221docker push <ACR>.azurecr.io/testapp:latest
222```
223## Quick Deployment (NixOS)
224```nix
225{
226 inputs = {
227 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
228 bluepds = {
229 url = "github:Teqed/bluesky-pds";
230 };
231 };
232 outputs = {
233 nixpkgs,
234 bluepds,
235 ...
236 }: {
237 nixosConfigurations.mysystem = nixpkgs.lib.nixosSystem {
238 modules = [
239 ({ pkgs, ... }: {
240 config.services.bluepds = {
241 enable = true;
242 host_name = "pds.example.com";
243 listen_address = "0.0.0.0:8000";
244 test = "true"; # Set to false for production
245 };
246 })
247 ];
248 };
249 };
250}
251```