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```