an atproto pds written in F# (.NET 9) 馃
pds
fsharp
giraffe
dotnet
atproto
1<!-- markdownlint-disable MD033 -->
2# PDSharp
3
4A Personal Data Server (PDS) for the AT Protocol, written in F# with Giraffe.
5
6## Goal
7
8Build and deploy a single-user PDS that can host your AT Protocol repository, serve blobs, and federate with Bluesky.
9
10## Requirements
11
12.NET 9.0 SDK
13
14## Getting Started
15
16### Restore & Build the project
17
18```bash
19dotnet restore
20dotnet build
21```
22
23### Run the tests
24
25```bash
26dotnet test
27```
28
29### Run the Server
30
31```bash
32dotnet run --project PDSharp/PDSharp.fsproj
33```
34
35The server will start at `http://localhost:5000`.
36
37## Configuration
38
39The application uses `appsettings.json` and supports Environment Variable overrides.
40
41| Key | Env Var | Default | Description |
42| ----------- | ------------------- | ----------------------- | ------------------------- |
43| `DidHost` | `PDSHARP_DidHost` | `did:web:localhost` | The DID of the PDS itself |
44| `PublicUrl` | `PDSHARP_PublicUrl` | `http://localhost:5000` | Publicly reachable URL |
45
46Example `appsettings.json`:
47
48```json
49{
50 "PublicUrl": "http://localhost:5000",
51 "DidHost": "did:web:localhost"
52}
53```
54
55## API Testing
56
57<details>
58<summary>Server Info</summary>
59
60```bash
61curl http://localhost:5000/xrpc/com.atproto.server.describeServer
62```
63
64</details>
65
66### Record Operations
67
68<details>
69<summary>Create a record</summary>
70
71```bash
72curl -X POST http://localhost:5000/xrpc/com.atproto.repo.createRecord \
73 -H "Content-Type: application/json" \
74 -d '{"repo":"did:web:test","collection":"app.bsky.feed.post","record":{"text":"Hello, ATProto!"}}'
75```
76
77</details>
78
79<details>
80<summary>Get a record</summary>
81
82```bash
83curl "http://localhost:5000/xrpc/com.atproto.repo.getRecord?repo=did:web:test&collection=app.bsky.feed.post&rkey=<RKEY>"
84```
85
86</details>
87
88<details>
89<summary>Put a record</summary>
90
91```bash
92curl -X POST http://localhost:5000/xrpc/com.atproto.repo.putRecord \
93 -H "Content-Type: application/json" \
94 -d '{"repo":"did:web:test","collection":"app.bsky.feed.post","rkey":"my-post","record":{"text":"Updated!"}}'
95```
96
97</details>
98
99### Sync & CAR Export
100
101<details>
102<summary>Get entire repository as CAR</summary>
103
104```bash
105curl "http://localhost:5000/xrpc/com.atproto.sync.getRepo?did=did:web:test" -o repo.car
106```
107
108</details>
109
110<details>
111<summary>Get specific blocks</summary>
112
113```bash
114curl "http://localhost:5000/xrpc/com.atproto.sync.getBlocks?did=did:web:test&cids=<CID1>,<CID2>" -o blocks.car
115```
116
117</details>
118
119<details>
120<summary>Get a blob by CID</summary>
121
122```bash
123curl "http://localhost:5000/xrpc/com.atproto.sync.getBlob?did=did:web:test&cid=<BLOB_CID>"
124```
125
126</details>
127
128### Firehose (WebSocket)
129
130Subscribe to real-time commit events using [websocat](https://github.com/vi/websocat):
131
132<details>
133<summary>Open a WebSocket connection</summary>
134
135```bash
136websocat ws://localhost:5000/xrpc/com.atproto.sync.subscribeRepos
137```
138
139</details>
140
141<br />
142Then create/update records in another terminal to see CBOR-encoded commit events stream in real-time.
143
144<br />
145
146<details>
147<summary>Open a WebSocket connection with cursor for resumption</summary>
148
149```bash
150websocat "ws://localhost:5000/xrpc/com.atproto.sync.subscribeRepos?cursor=5"
151```
152
153</details>
154
155## Architecture
156
157<details>
158<summary>App (Giraffe)</summary>
159
160- `XrpcRouter`: `/xrpc/<NSID>` routing
161- `Auth`: Session management (JWTs)
162- `RepoApi`: Write/Read records (`putRecord`, `getRecord`)
163- `ServerApi`: Server meta (`describeServer`)
164
165</details>
166
167<details>
168<summary>Core (Pure F#)</summary>
169
170- `DidResolver`: Identity resolution
171- `RepoEngine`: MST, DAG-CBOR, CIDs, Blocks
172- `Models`: Data types for XRPC/Database
173
174</details>
175
176<details>
177<summary>Infra</summary>
178
179- SQLite/Postgres for persistence
180- S3/Disk for blob storage
181
182</details>