BYOK Personal Data Server (PDS) written in Go
ipfs
vow
atproto
pds
go
1# Vow
2
3> [!WARNING]
4> This is highly experimental software. Use with caution, especially during account migration.
5
6Vow is a Bring-Your-Own-Key (BYOK) PDS (Personal Data Server) for AT Protocol.
7The server never stores a private signing key; all repository transactions are signed by a user passkey.
8
9## Quick Start
10
11> [!NOTE]
12> Experiment with Vow using the following `LFPJ3REG-BFICVMGE` invite code on the test server [vowpds.srv.rbrt.fr](https://vowpds.srv.rbrt.fr).
13> The PDS accounts are cleared regularly.
14
15[](https://atcr.io/r/julien.rbrt.fr/vow)
16
17### Prerequisites
18
19- Docker and Docker Compose installed
20- A domain name pointing to your server
21
22### Installation
23
241. **Clone repository**
25
26```bash
27git clone https://pkg.rbrt.fr/vow.git
28cd vow
29```
30
312. **Create your configuration file**
32
33```bash
34cp .env.example .env
35```
36
373. **Edit `.env` with your settings**
38
39```bash
40VOW_DID="did:web:your-domain.com"
41VOW_HOSTNAME="your-domain.com"
42VOW_CONTACT_EMAIL="you@example.com"
43VOW_RELAYS="https://bsky.network"
44
45# Generate with: openssl rand -hex 16
46VOW_ADMIN_PASSWORD="your-secure-password"
47
48# Generate with: openssl rand -hex 32
49VOW_SESSION_SECRET="your-session-secret"
50```
51
524. **Start services**
53
54```bash
55docker compose pull
56docker compose up -d
57```
58
59This starts three services:
60
61- **ipfs** — a Kubo node for repo blocks and blobs
62- **vow** — the PDS
63- **create-invite** — creates an initial invite code on first run
64
655. **Get your invite code**
66
67On first run, an invite code is automatically created:
68
69```bash
70docker compose logs create-invite
71```
72
73Or check saved file:
74
75```bash
76cat keys/initial-invite-code.txt
77```
78
796. **Monitor services**
80
81```bash
82docker compose logs -f
83```
84
85### What Gets Set Up
86
87- **init-keys**: Generates rotation key and JWK on first run
88- **ipfs**: A Kubo node for repo blocks and blobs. The RPC API (port 5001) stays internal; gateway (port 8080) is exposed on `127.0.0.1:8081` for your reverse proxy.
89- **vow**: The main PDS service on port 8080
90- **create-invite**: Creates an initial invite code on first run
91
92### Data Persistence
93
94- `./keys/` — generated keys
95 - `rotation.key` — PDS rotation key
96 - `jwk.key` — JWK private key
97 - `initial-invite-code.txt` — first invite code (first run only)
98- `./data/` — SQLite metadata database
99- `/opt/ipfs` Docker volume — IPFS blocks and blobs
100
101### Reverse Proxy
102
103You need a reverse proxy (nginx, Caddy, etc.) in front of the PDS:
104
105| Service | Internal address | Purpose |
106| ------- | ---------------- | ----------------------------- |
107| vow | `127.0.0.1:8080` | AT Protocol PDS |
108| ipfs | `127.0.0.1:8081` | IPFS gateway for blob serving |
109
110Set `VOW_IPFS_GATEWAY_URL` to your public gateway URL so `sync.getBlob` redirects clients there instead of proxying through vow.
111
112## Configuration
113
114### Database
115
116Vow uses SQLite for relational metadata such as accounts, sessions, record indexes, and tokens.
117
118```bash
119VOW_DB_NAME="/data/vow/vow.db"
120```
121
122### IPFS Node
123
124```bash
125# URL of Kubo RPC API
126VOW_IPFS_NODE_URL="http://127.0.0.1:5001"
127
128# Optional: redirect sync.getBlob to a public gateway
129VOW_IPFS_GATEWAY_URL="https://ipfs.example.com"
130```
131
132### SMTP Email
133
134```bash
135VOW_SMTP_USER="your-smtp-username"
136VOW_SMTP_PASS="your-smtp-password"
137VOW_SMTP_HOST="smtp.example.com"
138VOW_SMTP_PORT="587"
139VOW_SMTP_EMAIL="noreply@example.com"
140VOW_SMTP_NAME="Vow PDS"
141```
142
143### BYOK (Bring Your Own Key)
144
145The PDS holds two keys:
146
147- **Rotation key** (`rotation.key`) — used for DID genesis operations and for signing the PLC operation that transfers control to the user's passkey during passkey registration.
148- **JWK key** (`jwk.key`) — a P-256 ECDSA key used exclusively to sign ATProto session JWTs (access and refresh tokens) and OAuth tokens. It has no role in repo writes or identity operations.
149
150Neither key is ever used to sign repo commits or service-auth JWTs.
151
152## How It Works
153
154### Browser-Based Signer
155
156The account page (`/account`) connects over WebSocket and runs entirely in the browser. No browser extension or extra software is needed — the user just keeps the tab open and signs commits automatically when prompted.
157
158### Key Flow
159
1601. **Before passkey registration** — PDS controls DID with its rotation key
1612. **After passkey registration** — User's passkey becomes the rotation key, derived via WebAuthn PRF extension. Only the user can modify their DID.
1623. **Signing commits** — Passkey authenticates user and provides PRF output. A deterministic signing key is derived from PRF output and used to sign commits.
163
164### Two-Key Model
165
166Vow implements a two-key model:
167
168| Property | PDS Server Key | Passkey-Derived Key |
169| ---------------------- | ------------------ | --------------------------- |
170| **DID slot** | `#atproto_service` | `#atproto` |
171| **Purpose** | Service-auth JWTs | Repo commits |
172| **Passkey required** | No | Yes (for repo writes) |
173| **Private key stored** | Yes (in `jwk.key`) | **No** (derived on-the-fly) |
174
175## Management Commands
176
177Create an invite code:
178
179```bash
180docker exec vow-pds /vow create-invite-code --uses 1
181```
182
183Reset a user's password:
184
185```bash
186docker exec vow-pds /vow reset-password --did "did:plc:xxx"
187```
188
189## Updating
190
191```bash
192docker compose build
193docker compose up -d
194```
195
196## Implemented Endpoints
197
198> [!NOTE]
199> Just because something is implemented doesn't mean it is finished. Many endpoints still have rough edges around validation and error handling.
200
201### Identity
202
203- [x] `com.atproto.identity.getRecommendedDidCredentials`
204- [x] `com.atproto.identity.requestPlcOperationSignature`
205- [x] `com.atproto.identity.resolveHandle`
206- [x] `com.atproto.identity.signPlcOperation`
207- [x] `com.atproto.identity.submitPlcOperation`
208- [x] `com.atproto.identity.updateHandle`
209
210### Repo
211
212- [x] `com.atproto.repo.applyWrites`
213- [x] `com.atproto.repo.createRecord`
214- [x] `com.atproto.repo.putRecord`
215- [x] `com.atproto.repo.deleteRecord`
216- [x] `com.atproto.repo.describeRepo`
217- [x] `com.atproto.repo.getRecord`
218- [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.)
219- [x] `com.atproto.repo.listRecords`
220- [x] `com.atproto.repo.listMissingBlobs`
221
222### Server
223
224- [x] `com.atproto.server.activateAccount`
225- [x] `com.atproto.server.checkAccountStatus`
226- [x] `com.atproto.server.confirmEmail`
227- [x] `com.atproto.server.createAccount`
228- [x] `com.atproto.server.createInviteCode`
229- [x] `com.atproto.server.createInviteCodes`
230- [x] `com.atproto.server.deactivateAccount`
231- [x] `com.atproto.server.deleteAccount`
232- [x] `com.atproto.server.deleteSession`
233- [x] `com.atproto.server.describeServer`
234- [x] `com.atproto.server.getAccountInviteCodes`
235- [x] `com.atproto.server.getServiceAuth`
236- [x] `com.atproto.server.refreshSession`
237- [x] `com.atproto.server.requestAccountDelete`
238- [x] `com.atproto.server.requestEmailConfirmation`
239- [x] `com.atproto.server.requestEmailUpdate`
240- [x] `com.atproto.server.requestPasswordReset`
241- [x] `com.atproto.server.resetPassword`
242- [x] `com.atproto.server.updateEmail`
243
244### Sync
245
246- [x] `com.atproto.sync.getBlob`
247- [x] `com.atproto.sync.getBlocks`
248- [x] `com.atproto.sync.getLatestCommit`
249- [x] `com.atproto.sync.getRecord`
250- [x] `com.atproto.sync.getRepoStatus`
251- [x] `com.atproto.sync.getRepo`
252- [x] `com.atproto.sync.listBlobs`
253- [x] `com.atproto.sync.requestCrawl`
254- [x] `com.atproto.sync.subscribeRepos`
255
256### Other
257
258- [x] `com.atproto.label.queryLabels`
259- [x] `com.atproto.moderation.createReport`
260- [x] `app.bsky.actor.getPreferences`
261- [x] `app.bsky.actor.putPreferences`
262
263## License
264
265[MIT](license). `server/static/pico.css` is also MIT licensed, available at [https://github.com/picocss/pico](https://github.com/picocss/pico).
266
267## Thanks
268
269Vow is based on [Cocoon](https://tangled.org/hailey.at/cocoon). Many thanks for the solid foundation.
270
271### Vow vs Cocoon
272
273| Feature | Vow | Cocoon |
274| ----------------------- | ---------- | ------ |
275| Language | Go | Go |
276| SQLite (metadata) | ✅ | ✅ |
277| SQLite blockstore | ❌ removed | ✅ |
278| PostgreSQL support | ❌ removed | ✅ |
279| S3 blob storage | ❌ removed | ✅ |
280| IPFS repo block storage | ✅ (Kubo) | ❌ |
281| IPFS blob storage | ✅ (Kubo) | ❌ |
282| Email 2FA | ❌ removed | ✅ |
283| BYOK (keyless PDS) | ✅ | ❌ |
284| Passkey signer | ✅ | ❌ |
285
286## Technical Details
287
288For in-depth specifications, flows, trade-offs, and maintenance considerations, see [specs.md](specs.md).