Vow#
WARNING
This is highly experimental software. Use with caution, especially during account migration.
IMPORTANT
Vow implements a two-key model for signing using WebAuthn PRF extension. After registering a passkey, the server never stores a private signing key — the passkey authenticates and provides PRF output, from which a deterministic signing key is derived on-the-fly for each commit. Users fully control their DID.
Vow is a Go PDS (Personal Data Server) for AT Protocol.
Quick Start with Docker Compose#
Prerequisites#
- Docker and Docker Compose installed
- A domain name pointing to your server
Installation#
- Clone repository
git clone https://pkg.rbrt.fr/vow.git
cd vow
- Create your configuration file
cp .env.example .env
- Edit
.envwith your settings
VOW_DID="did:web:your-domain.com"
VOW_HOSTNAME="your-domain.com"
VOW_CONTACT_EMAIL="you@example.com"
VOW_RELAYS="https://bsky.network"
# Generate with: openssl rand -hex 16
VOW_ADMIN_PASSWORD="your-secure-password"
# Generate with: openssl rand -hex 32
VOW_SESSION_SECRET="your-session-secret"
- Start services
docker compose pull
docker compose up -d
This starts three services:
- ipfs — a Kubo node for repo blocks and blobs
- vow — the PDS
- create-invite — creates an initial invite code on first run
- Get your invite code
On first run, an invite code is automatically created:
docker compose logs create-invite
Or check saved file:
cat keys/initial-invite-code.txt
- Monitor services
docker compose logs -f
What Gets Set Up#
- init-keys: Generates rotation key and JWK on first run
- 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:8081for your reverse proxy. - vow: The main PDS service on port 8080
- create-invite: Creates an initial invite code on first run
Data Persistence#
./keys/— generated keysrotation.key— PDS rotation keyjwk.key— JWK private keyinitial-invite-code.txt— first invite code (first run only)
./data/— SQLite metadata databaseipfs_dataDocker volume — IPFS blocks and blobs
Reverse Proxy#
You need a reverse proxy (nginx, Caddy, etc.) in front of the PDS:
| Service | Internal address | Purpose |
|---|---|---|
| vow | 127.0.0.1:8080 |
AT Protocol PDS |
| ipfs | 127.0.0.1:8081 |
IPFS gateway for blob serving |
Set VOW_IPFS_GATEWAY_URL to your public gateway URL so sync.getBlob redirects clients there instead of proxying through vow.
Configuration#
Database#
Vow uses SQLite for relational metadata such as accounts, sessions, record indexes, and tokens.
VOW_DB_NAME="/data/vow/vow.db"
IPFS Node#
# URL of Kubo RPC API
VOW_IPFS_NODE_URL="http://127.0.0.1:5001"
# Optional: redirect sync.getBlob to a public gateway
VOW_IPFS_GATEWAY_URL="https://ipfs.example.com"
SMTP Email#
VOW_SMTP_USER="your-smtp-username"
VOW_SMTP_PASS="your-smtp-password"
VOW_SMTP_HOST="smtp.example.com"
VOW_SMTP_PORT="587"
VOW_SMTP_EMAIL="noreply@example.com"
VOW_SMTP_NAME="Vow PDS"
BYOK (Bring Your Own Key)#
The PDS holds two keys:
- 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. - 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.
Neither key is ever used to sign repo commits or service-auth JWTs.
How It Works#
Browser-Based Signer#
The 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.
Key Flow#
- Before passkey registration — PDS controls DID with its rotation key
- After passkey registration — User's passkey becomes the rotation key, derived via WebAuthn PRF extension. Only the user can modify their DID.
- Signing commits — Passkey authenticates user and provides PRF output. A deterministic signing key is derived from PRF output and used to sign commits.
Two-Key Model#
Vow implements a two-key model:
| Property | PDS Server Key | Passkey-Derived Key |
|---|---|---|
| DID slot | #atproto_service |
#atproto |
| Purpose | Service-auth JWTs | Repo commits |
| Passkey required | No | Yes (for repo writes) |
| Private key stored | Yes (in jwk.key) |
No (derived on-the-fly) |
Management Commands#
Create an invite code:
docker exec vow-pds /vow create-invite-code --uses 1
Reset a user's password:
docker exec vow-pds /vow reset-password --did "did:plc:xxx"
Updating#
docker compose build
docker compose up -d
Implemented Endpoints#
NOTE
Just because something is implemented doesn't mean it is finished. Many endpoints still have rough edges around validation and error handling.
Identity#
-
com.atproto.identity.getRecommendedDidCredentials -
com.atproto.identity.requestPlcOperationSignature -
com.atproto.identity.resolveHandle -
com.atproto.identity.signPlcOperation -
com.atproto.identity.submitPlcOperation -
com.atproto.identity.updateHandle
Repo#
-
com.atproto.repo.applyWrites -
com.atproto.repo.createRecord -
com.atproto.repo.putRecord -
com.atproto.repo.deleteRecord -
com.atproto.repo.describeRepo -
com.atproto.repo.getRecord -
com.atproto.repo.importRepo(Works "okay". Use with extreme caution.) -
com.atproto.repo.listRecords -
com.atproto.repo.listMissingBlobs
Server#
-
com.atproto.server.activateAccount -
com.atproto.server.checkAccountStatus -
com.atproto.server.confirmEmail -
com.atproto.server.createAccount -
com.atproto.server.createInviteCode -
com.atproto.server.createInviteCodes -
com.atproto.server.deactivateAccount -
com.atproto.server.deleteAccount -
com.atproto.server.deleteSession -
com.atproto.server.describeServer -
com.atproto.server.getAccountInviteCodes -
com.atproto.server.getServiceAuth -
com.atproto.server.refreshSession -
com.atproto.server.requestAccountDelete -
com.atproto.server.requestEmailConfirmation -
com.atproto.server.requestEmailUpdate -
com.atproto.server.requestPasswordReset -
com.atproto.server.resetPassword -
com.atproto.server.updateEmail
Sync#
-
com.atproto.sync.getBlob -
com.atproto.sync.getBlocks -
com.atproto.sync.getLatestCommit -
com.atproto.sync.getRecord -
com.atproto.sync.getRepoStatus -
com.atproto.sync.getRepo -
com.atproto.sync.listBlobs -
com.atproto.sync.requestCrawl -
com.atproto.sync.subscribeRepos
Other#
-
com.atproto.label.queryLabels -
com.atproto.moderation.createReport -
app.bsky.actor.getPreferences -
app.bsky.actor.putPreferences
License#
MIT. server/static/pico.css is also MIT licensed, available at https://github.com/picocss/pico.
Thanks#
Vow is based on Cocoon. Many thanks for the solid foundation.
Vow vs Cocoon#
| Feature | Vow | Cocoon |
|---|---|---|
| Language | Go | Go |
| SQLite (metadata) | ✅ | ✅ |
| SQLite blockstore | ❌ removed | ✅ |
| PostgreSQL support | ❌ removed | ✅ |
| S3 blob storage | ❌ removed | ✅ |
| IPFS repo block storage | ✅ (Kubo) | ❌ |
| IPFS blob storage | ✅ (Kubo) | ❌ |
| Email 2FA | ❌ removed | ✅ |
| BYOK (keyless PDS) | ✅ | ❌ |
| Passkey signer | ✅ | ❌ |
Technical Details#
For in-depth specifications, flows, trade-offs, and maintenance considerations, see specs.md.