atproto pds in zig pds.zat.dev
pds atproto
24

Configure Feed

Select the types of activity you want to include in your feed.

docs: align implementation notes

zzstoatzz (May 27, 2026, 4:58 PM -0500) 29006057 64abebb3

+139 -34
+5 -2
AGENTS.md
··· 30 30 - `atproto/proxy`: generic service proxying through `atproto-proxy` 31 31 - `storage`: SQLite state, repo commits, MST updates, blobstore metadata 32 32 - `internal`: ZDS-local utilities that keep feature code small; use this for 33 - reusable glue such as CLI parsing, sharded locks, and JOSE compatibility 34 - helpers before deciding whether a primitive belongs in `zat`. 33 + reusable glue such as CLI parsing, sharded locks, passkey adapters, and JOSE 34 + compatibility helpers before deciding whether a primitive belongs in `zat`. 35 35 - `bench`, `tools`, `justfile`: local smoke tests, admin operations, and 36 36 benchmark entry points. Prefer Just targets for repeatable workflows. 37 37 ··· 46 46 `validationStatus: "valid"` without validation. 47 47 - Invite-required deployments must advertise `inviteCodeRequired: true` and 48 48 consume invite codes during account creation. 49 + - Passkeys are stored as account credentials: credential ID, public key, 50 + signature counter, friendly name, and timestamps. Password login remains 51 + available unless an explicit account policy changes that. 49 52 - OAuth should follow the ATProto OAuth profile and JOSE behavior without 50 53 relaxing stricter repo-signature verification semantics. 51 54 - Appview behavior belongs behind the generic proxy path, not in local
+5 -1
README.md
··· 7 7 `zds` stores atproto accounts, repos, records, blobs, sessions, oauth state, and 8 8 identity state. it uses [`zat`](../zat) for protocol primitives: syntax, tids, 9 9 did resolution, jwt helpers, dag-cbor, car, mst, repo verification, and key 10 - encoding. 10 + encoding. WebAuthn support comes from 11 + [`webauthn`](https://tangled.org/zzstoatzz.io/webauthn). 11 12 12 13 ## docs 13 14 ··· 15 16 - [development](docs/development.md) 16 17 - [operations](docs/operations.md) 17 18 - [invite codes](docs/invite-codes.md) 19 + - [passkeys](docs/passkeys.md) 18 20 - [references](docs/references.md) 19 21 - [benchmarks](bench/README.md) 20 22 ··· 100 102 - Invite codes use the official PDS table shape: code metadata plus recorded 101 103 uses. When invites are required, `createAccount` rejects missing, disabled, or 102 104 exhausted codes. 105 + - Passkeys are optional account credentials for the OAuth login page. ZDS stores 106 + credential IDs, public keys, counters, names, and last-use timestamps. 103 107 104 108 ## references 105 109
+11 -17
bench/README.md
··· 38 38 - `repo`: full repo CAR materialization through `writeRepoCar`. 39 39 - `blob`: blob write/read against disk blobstore plus SQLite metadata. 40 40 - `metastore`: Tranquil-shaped apply/get/list benchmark with caller counts and 41 - latency percentiles. Its get/list rows are ZDS-local probes, not all direct 42 - Tranquil comparisons. 41 + latency percentiles. 43 42 - `get-cid`: CID-only record lookup, matching Tranquil's `get_record_cid` 44 43 metastore benchmark. 45 44 - `get-block`: record index lookup plus `repo_blocks` byte fetch. ··· 110 109 just bench get-cid 100 200 111 110 ``` 112 111 113 - This matrix only includes operation/count pairs measured with the same unit of 114 - work. Tranquil's `get_record_cid` and `list_records` are record-index 115 - operations: `list_records` returns `rkey + record_cid`, not record body JSON. 116 - The official PDS probe measures its actor-store read paths with Vitest. Its 117 - single-operation rows are apples-to-apples for the internal read boundaries. Its 118 - batched CID rows are useful throughput shape checks, but their p99 values are 119 - batch latencies, not per-operation latencies from the ZDS/Tranquil harness. 112 + This matrix includes only operation/count pairs measured with the same unit of 113 + work. Tranquil's `get_record_cid` is a record-index operation. The official PDS 114 + probe measures its actor-store read paths with Vitest; its single-operation rows 115 + are included for matching internal read/write boundaries. 120 116 121 117 Summary: ZDS is fastest in the single-caller read probes. Tranquil is fastest in 122 118 the direct 10/100 caller CID lookup probes. The official PDS is slower in these ··· 130 126 | apply one record commit | 1 | 642 ops/s, p95 2.9 ms | 287 ops/s, p95 4.1 ms | 448 ops/s, p99 4.0 ms | 131 127 | current record CID lookup | 1 | 414k ops/s, p95 4 us | 96.8k ops/s, p95 15 us | 3.1k ops/s, p99 511 us | 132 128 133 - The direct caller-count concurrency matrix is only ZDS and Tranquil. The 134 - official-PDS rows below it use a batched Vitest probe and are kept separate. 129 + The direct caller-count concurrency matrix is ZDS and Tranquil only. Official 130 + PDS concurrency probes are kept separate until they use the same caller model. 135 131 136 132 | operation | callers | ops | zds | tranquil | 137 133 |---|---:|---:|---:|---:| ··· 148 144 | current record CID lookup | 100 callers x 20 lookups | 3.2k lookups/s | 630 ms | 149 145 150 146 Tranquil's metastore `list_records` benchmark is not in this table because the 151 - current ZDS list probe follows the official Bluesky PDS read shape: join the 152 - record index to `repo_blocks`, decode DAG-CBOR, and return record values. An 153 - index-only list probe should be reported separately when it exists. 147 + ZDS row reports the official-PDS-shaped `listRecords`: record index lookup, 148 + `repo_blocks` fetch, DAG-CBOR decode, and JSON materialization. Add an 149 + index-only ZDS list probe before comparing that row. 154 150 155 151 ## official PDS probe 156 152 ··· 171 167 | full getRecord materialization | 3.0k ops/s, p99 525 us | 172 168 | full listRecords, limit 50 | 635 ops/s, p99 2.4 ms | 173 169 174 - These are useful for the official-PDS column, but they should not be mixed with 175 - ZDS/Tranquil direct caller-count latency rows. The matching ZDS single-caller 176 - rows are: 170 + The matching ZDS single-caller rows are: 177 171 178 172 | operation | zds | 179 173 |---|---:|
+22 -5
docs/architecture.md
··· 13 13 helpers, DAG-CBOR, CAR, MST, repo verification, OAuth helpers, and streaming 14 14 clients. 15 15 - `zds` owns PDS policy and persistence: accounts, repo writes, sessions, 16 - storage, server routes, sync production, migration, and compatibility 17 - behavior. 16 + storage, server routes, sync production, account migration, and the local 17 + login/security surface. 18 18 - SQLite stores account, repo, commit, token, OAuth, blob metadata, and identity 19 19 state. 20 20 - Invite codes follow the official PDS split between code metadata and recorded 21 21 uses; account creation consumes a code while holding the store lock. 22 22 - Blob bytes live in the disk blobstore rooted at `ZDS_BLOBSTORE_PATH`. 23 - - JOSE compatibility helpers live under `internal` until they are clean enough 24 - to upstream to `zat`; OAuth JWT verification must accept normal JOSE ECDSA 25 - signatures without relaxing repo signature verification. 23 + - Passkey storage follows Tranquil's core shape: credential ID, public key, 24 + signature counter, friendly name, and timestamps. Passkeys are outside the 25 + official PDS surface ZDS tracks for compatibility. 26 26 27 27 ## repo write invariants 28 28 ··· 47 47 visibility depends on valid repo commits, coherent event frames, and crawl 48 48 requests. `ZDS_CRAWLERS` should include both `https://bsky.network` and 49 49 `https://vsky.network` for current deployment coverage. 50 + 51 + ## auth and login 52 + 53 + OAuth follows the ATProto OAuth profile: PAR is required, redirect URIs are 54 + validated against client metadata, permission-set includes must resolve, tokens 55 + enforce granular repo/blob/rpc/account/identity scopes, and revocation affects 56 + resource-server checks. 57 + 58 + Password sessions and app passwords match the reference PDS account model. 59 + Passkeys are an additional OAuth login credential inspired by Tranquil, backed 60 + by the local `webauthn` dependency and stored in SQLite. Discoverable passkey 61 + login is supported so an account can be chosen by the authenticator during the 62 + OAuth flow. 63 + 64 + PDS-owned `app.bsky.actor` preferences are handled locally because the official 65 + PDS implements those routes. Appview reads and writes remain behind the generic 66 + `atproto-proxy` path.
+7 -5
docs/benchmarking.md
··· 13 13 14 14 ## current matrix 15 15 16 - The active three-way matrix is: 16 + The active apples-to-apples matrix is intentionally small: 17 17 18 18 - apply one record commit 19 19 - current record CID lookup 20 + 21 + Adjacent probes are tracked separately: 22 + 20 23 - record-index list 21 24 - full record materialization 22 25 - blob/block put and get 23 26 - repo export 24 27 - sync event delivery 25 28 26 - ZDS, Tranquil, and the official PDS now have matching single-caller numbers for 29 + ZDS, Tranquil, and the official PDS have matching single-caller numbers for 27 30 apply-one-record and current-CID lookup. ZDS and Tranquil also have direct 28 - 10/100 caller concurrency curves. The official probe has a batched CID lookup 29 - shape check, but its latency values are batch latencies from Vitest, not per-op 30 - latencies from the same harness as the ZDS and Tranquil rows. 31 + 10/100 caller concurrency curves. Official-PDS batch probes stay outside the 32 + main matrix until they are measured through the same unit of work. 31 33 32 34 ## read path 33 35
+24 -1
docs/development.md
··· 17 17 blob metadata. Runtime code reads record bodies from `repo_blocks`, not from 18 18 cached JSON columns. 19 19 - `src/internal` is ZDS-local reusable glue. Good candidates are CLI parsing, 20 - sharded synchronization, JOSE compatibility, and narrow encoding adapters. 20 + sharded synchronization, passkey adapters, and narrow encoding adapters. 21 21 Protocol primitives still belong in `zat` once they are general enough. 22 22 - `bench` and `tools` contain repeatable local probes. Prefer Just targets over 23 23 one-off command lines. ··· 32 32 When ZDS needs a generally useful primitive that `zat` does not expose yet, 33 33 build the smallest local version under `src/internal`, use it from ZDS, and 34 34 document why it is likely upstream material. 35 + 36 + For generated JSON, prefer typed structs plus `std.json.Stringify.valueAlloc` 37 + or a narrow helper over hand-written object strings. `std.json.fmt` is still 38 + useful at stream boundaries, but new response code should avoid assembling JSON 39 + objects with format strings when a structured value is straightforward. 35 40 36 41 ## lessons from history 37 42 ··· 43 48 `validationStatus: "valid"` without validation. 44 49 - OAuth client assertions are JOSE signatures. Accept standard raw ECDSA JWS 45 50 signatures for OAuth without weakening stricter repo-signature verification. 51 + - Passkey support should stay close to Tranquil's WebAuthn model: account-owned 52 + credential rows plus challenge rows, with browser prompts opened directly from 53 + user gestures. 46 54 - Storage shape follows the official PDS SQLite model where practical: record 47 55 index rows identify current CIDs, and canonical record content is stored as 48 56 DAG-CBOR blocks. ··· 54 62 Local SQLite files under `dev/` are ignored workspace state. Do not commit 55 63 database files, WAL files, `.env`, `.zig-cache`, `zig-out`, or package cache 56 64 directories. 65 + 66 + ## commands 67 + 68 + The root `justfile` is the stable task surface: 69 + 70 + ```sh 71 + just test 72 + just smoke 73 + just invite https://pds.zat.dev 74 + just plc-repair did:plc:... 75 + just bench all 76 + just docker-publish-current 77 + ``` 78 + 79 + Run `zig zen` before committing.
+16 -3
docs/operations.md
··· 5 5 Run these before committing: 6 6 7 7 ```sh 8 - zig build test 9 - tools/smoke.sh 8 + just test 9 + just smoke 10 10 zig zen 11 11 ``` 12 12 13 - `tools/smoke.sh` starts a local server, creates a session, writes records, 13 + `just smoke` starts a local server, creates a session, writes records, 14 14 checks blob upload, verifies repo/sync endpoints, and asserts that known app 15 15 records use valid key shapes. 16 16 ··· 72 72 including invite-code minting. 73 73 - `ZDS_INVITE_REQUIRED`: set to `true` to require invite codes for account 74 74 creation. `describeServer` reflects this value. 75 + - Passkeys do not require a separate deployment secret. WebAuthn RP ID is 76 + derived from the public URL host. 75 77 76 78 PLC key lifecycle: 77 79 ··· 126 128 127 129 Migration requires working public HTTPS, stable JWT secret, email delivery, blob 128 130 storage, and writable SQLite storage. 131 + 132 + ## conformance 133 + 134 + Useful external checks: 135 + 136 + - `https://check.cirrus.earth/?target=<handle>` for PDS and OAuth conformance. 137 + - bsky.app and pdsls.dev for browser-visible OAuth, proxy, repo, and blob 138 + behavior. 139 + 140 + Treat browser failures as request/response evidence: inspect the failing XRPC, 141 + status code, response body, and relevant server logs before changing behavior.
+46
docs/passkeys.md
··· 1 + # passkeys 2 + 3 + ZDS supports WebAuthn passkeys for OAuth login. This is modeled after 4 + Tranquil's passkey support; passkeys are outside the official Bluesky PDS 5 + surface ZDS tracks for compatibility. 6 + 7 + ## storage 8 + 9 + Each passkey row belongs to an account DID and stores: 10 + 11 + - credential ID 12 + - credential public key 13 + - signature counter 14 + - friendly name 15 + - created and last-used timestamps 16 + 17 + Challenge rows are short-lived. Registration challenges are tied to the account 18 + DID. Discoverable login challenges are tied to the OAuth request so an 19 + authenticator can identify the account by credential ID. 20 + 21 + ## login behavior 22 + 23 + If the login hint resolves to an account with saved passkeys, the OAuth page 24 + offers passkey login first and keeps password login available. If no passkey is 25 + saved for the account, password login is shown directly with a link to the 26 + security page. 27 + 28 + Discoverable passkey login is supported: the browser may return a credential 29 + without the page first choosing an account. ZDS resolves the credential ID to 30 + the stored account, verifies the assertion, updates the signature counter, and 31 + authorizes the OAuth request. 32 + 33 + ## management 34 + 35 + Passkeys are managed from `/security`. That page signs in with the account 36 + password, lists saved passkeys, and supports add, rename, and delete. 37 + 38 + Deleting a passkey removes it from ZDS. The browser, OS, or password manager 39 + may still keep its copy, but it can no longer be used with this PDS. 40 + 41 + ## references 42 + 43 + - Tranquil frontend: `frontend/src/routes/OAuthLogin.svelte`, 44 + `frontend/src/routes/Settings.svelte` 45 + - Tranquil storage traits: `crates/tranquil-db-traits/src/user.rs` 46 + - ZDS WebAuthn dependency: <https://tangled.org/zzstoatzz.io/webauthn>
+3
docs/references.md
··· 47 47 - Invite codes are core PDS state. Captcha, 2FA, and migration-only policies can 48 48 live at the PDS boundary or a reverse proxy boundary; pds-gatekeeper is useful 49 49 prior art for that layer, not a substitute for invite-code accounting. 50 + - Passkeys are Tranquil-derived, not reference-PDS-derived: store credential 51 + IDs, public keys, counters, names, and timestamps, and verify WebAuthn 52 + assertions before authorizing OAuth requests. 50 53 51 54 ## Tranquil comparison 52 55