···3030- `atproto/proxy`: generic service proxying through `atproto-proxy`
3131- `storage`: SQLite state, repo commits, MST updates, blobstore metadata
3232- `internal`: ZDS-local utilities that keep feature code small; use this for
3333- reusable glue such as CLI parsing, sharded locks, and JOSE compatibility
3434- helpers before deciding whether a primitive belongs in `zat`.
3333+ reusable glue such as CLI parsing, sharded locks, passkey adapters, and JOSE
3434+ compatibility helpers before deciding whether a primitive belongs in `zat`.
3535- `bench`, `tools`, `justfile`: local smoke tests, admin operations, and
3636 benchmark entry points. Prefer Just targets for repeatable workflows.
3737···4646 `validationStatus: "valid"` without validation.
4747- Invite-required deployments must advertise `inviteCodeRequired: true` and
4848 consume invite codes during account creation.
4949+- Passkeys are stored as account credentials: credential ID, public key,
5050+ signature counter, friendly name, and timestamps. Password login remains
5151+ available unless an explicit account policy changes that.
4952- OAuth should follow the ATProto OAuth profile and JOSE behavior without
5053 relaxing stricter repo-signature verification semantics.
5154- Appview behavior belongs behind the generic proxy path, not in local
+5-1
README.md
···77`zds` stores atproto accounts, repos, records, blobs, sessions, oauth state, and
88identity state. it uses [`zat`](../zat) for protocol primitives: syntax, tids,
99did resolution, jwt helpers, dag-cbor, car, mst, repo verification, and key
1010-encoding.
1010+encoding. WebAuthn support comes from
1111+[`webauthn`](https://tangled.org/zzstoatzz.io/webauthn).
11121213## docs
1314···1516- [development](docs/development.md)
1617- [operations](docs/operations.md)
1718- [invite codes](docs/invite-codes.md)
1919+- [passkeys](docs/passkeys.md)
1820- [references](docs/references.md)
1921- [benchmarks](bench/README.md)
2022···100102- Invite codes use the official PDS table shape: code metadata plus recorded
101103 uses. When invites are required, `createAccount` rejects missing, disabled, or
102104 exhausted codes.
105105+- Passkeys are optional account credentials for the OAuth login page. ZDS stores
106106+ credential IDs, public keys, counters, names, and last-use timestamps.
103107104108## references
105109
+11-17
bench/README.md
···3838- `repo`: full repo CAR materialization through `writeRepoCar`.
3939- `blob`: blob write/read against disk blobstore plus SQLite metadata.
4040- `metastore`: Tranquil-shaped apply/get/list benchmark with caller counts and
4141- latency percentiles. Its get/list rows are ZDS-local probes, not all direct
4242- Tranquil comparisons.
4141+ latency percentiles.
4342- `get-cid`: CID-only record lookup, matching Tranquil's `get_record_cid`
4443 metastore benchmark.
4544- `get-block`: record index lookup plus `repo_blocks` byte fetch.
···110109just bench get-cid 100 200
111110```
112111113113-This matrix only includes operation/count pairs measured with the same unit of
114114-work. Tranquil's `get_record_cid` and `list_records` are record-index
115115-operations: `list_records` returns `rkey + record_cid`, not record body JSON.
116116-The official PDS probe measures its actor-store read paths with Vitest. Its
117117-single-operation rows are apples-to-apples for the internal read boundaries. Its
118118-batched CID rows are useful throughput shape checks, but their p99 values are
119119-batch latencies, not per-operation latencies from the ZDS/Tranquil harness.
112112+This matrix includes only operation/count pairs measured with the same unit of
113113+work. Tranquil's `get_record_cid` is a record-index operation. The official PDS
114114+probe measures its actor-store read paths with Vitest; its single-operation rows
115115+are included for matching internal read/write boundaries.
120116121117Summary: ZDS is fastest in the single-caller read probes. Tranquil is fastest in
122118the direct 10/100 caller CID lookup probes. The official PDS is slower in these
···130126| 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 |
131127| 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 |
132128133133-The direct caller-count concurrency matrix is only ZDS and Tranquil. The
134134-official-PDS rows below it use a batched Vitest probe and are kept separate.
129129+The direct caller-count concurrency matrix is ZDS and Tranquil only. Official
130130+PDS concurrency probes are kept separate until they use the same caller model.
135131136132| operation | callers | ops | zds | tranquil |
137133|---|---:|---:|---:|---:|
···148144| current record CID lookup | 100 callers x 20 lookups | 3.2k lookups/s | 630 ms |
149145150146Tranquil's metastore `list_records` benchmark is not in this table because the
151151-current ZDS list probe follows the official Bluesky PDS read shape: join the
152152-record index to `repo_blocks`, decode DAG-CBOR, and return record values. An
153153-index-only list probe should be reported separately when it exists.
147147+ZDS row reports the official-PDS-shaped `listRecords`: record index lookup,
148148+`repo_blocks` fetch, DAG-CBOR decode, and JSON materialization. Add an
149149+index-only ZDS list probe before comparing that row.
154150155151## official PDS probe
156152···171167| full getRecord materialization | 3.0k ops/s, p99 525 us |
172168| full listRecords, limit 50 | 635 ops/s, p99 2.4 ms |
173169174174-These are useful for the official-PDS column, but they should not be mixed with
175175-ZDS/Tranquil direct caller-count latency rows. The matching ZDS single-caller
176176-rows are:
170170+The matching ZDS single-caller rows are:
177171178172| operation | zds |
179173|---|---:|
+22-5
docs/architecture.md
···1313 helpers, DAG-CBOR, CAR, MST, repo verification, OAuth helpers, and streaming
1414 clients.
1515- `zds` owns PDS policy and persistence: accounts, repo writes, sessions,
1616- storage, server routes, sync production, migration, and compatibility
1717- behavior.
1616+ storage, server routes, sync production, account migration, and the local
1717+ login/security surface.
1818- SQLite stores account, repo, commit, token, OAuth, blob metadata, and identity
1919 state.
2020- Invite codes follow the official PDS split between code metadata and recorded
2121 uses; account creation consumes a code while holding the store lock.
2222- Blob bytes live in the disk blobstore rooted at `ZDS_BLOBSTORE_PATH`.
2323-- JOSE compatibility helpers live under `internal` until they are clean enough
2424- to upstream to `zat`; OAuth JWT verification must accept normal JOSE ECDSA
2525- signatures without relaxing repo signature verification.
2323+- Passkey storage follows Tranquil's core shape: credential ID, public key,
2424+ signature counter, friendly name, and timestamps. Passkeys are outside the
2525+ official PDS surface ZDS tracks for compatibility.
26262727## repo write invariants
2828···4747visibility depends on valid repo commits, coherent event frames, and crawl
4848requests. `ZDS_CRAWLERS` should include both `https://bsky.network` and
4949`https://vsky.network` for current deployment coverage.
5050+5151+## auth and login
5252+5353+OAuth follows the ATProto OAuth profile: PAR is required, redirect URIs are
5454+validated against client metadata, permission-set includes must resolve, tokens
5555+enforce granular repo/blob/rpc/account/identity scopes, and revocation affects
5656+resource-server checks.
5757+5858+Password sessions and app passwords match the reference PDS account model.
5959+Passkeys are an additional OAuth login credential inspired by Tranquil, backed
6060+by the local `webauthn` dependency and stored in SQLite. Discoverable passkey
6161+login is supported so an account can be chosen by the authenticator during the
6262+OAuth flow.
6363+6464+PDS-owned `app.bsky.actor` preferences are handled locally because the official
6565+PDS implements those routes. Appview reads and writes remain behind the generic
6666+`atproto-proxy` path.
+7-5
docs/benchmarking.md
···13131414## current matrix
15151616-The active three-way matrix is:
1616+The active apples-to-apples matrix is intentionally small:
17171818- apply one record commit
1919- current record CID lookup
2020+2121+Adjacent probes are tracked separately:
2222+2023- record-index list
2124- full record materialization
2225- blob/block put and get
2326- repo export
2427- sync event delivery
25282626-ZDS, Tranquil, and the official PDS now have matching single-caller numbers for
2929+ZDS, Tranquil, and the official PDS have matching single-caller numbers for
2730apply-one-record and current-CID lookup. ZDS and Tranquil also have direct
2828-10/100 caller concurrency curves. The official probe has a batched CID lookup
2929-shape check, but its latency values are batch latencies from Vitest, not per-op
3030-latencies from the same harness as the ZDS and Tranquil rows.
3131+10/100 caller concurrency curves. Official-PDS batch probes stay outside the
3232+main matrix until they are measured through the same unit of work.
31333234## read path
3335
+24-1
docs/development.md
···1717 blob metadata. Runtime code reads record bodies from `repo_blocks`, not from
1818 cached JSON columns.
1919- `src/internal` is ZDS-local reusable glue. Good candidates are CLI parsing,
2020- sharded synchronization, JOSE compatibility, and narrow encoding adapters.
2020+ sharded synchronization, passkey adapters, and narrow encoding adapters.
2121 Protocol primitives still belong in `zat` once they are general enough.
2222- `bench` and `tools` contain repeatable local probes. Prefer Just targets over
2323 one-off command lines.
···3232When ZDS needs a generally useful primitive that `zat` does not expose yet,
3333build the smallest local version under `src/internal`, use it from ZDS, and
3434document why it is likely upstream material.
3535+3636+For generated JSON, prefer typed structs plus `std.json.Stringify.valueAlloc`
3737+or a narrow helper over hand-written object strings. `std.json.fmt` is still
3838+useful at stream boundaries, but new response code should avoid assembling JSON
3939+objects with format strings when a structured value is straightforward.
35403641## lessons from history
3742···4348 `validationStatus: "valid"` without validation.
4449- OAuth client assertions are JOSE signatures. Accept standard raw ECDSA JWS
4550 signatures for OAuth without weakening stricter repo-signature verification.
5151+- Passkey support should stay close to Tranquil's WebAuthn model: account-owned
5252+ credential rows plus challenge rows, with browser prompts opened directly from
5353+ user gestures.
4654- Storage shape follows the official PDS SQLite model where practical: record
4755 index rows identify current CIDs, and canonical record content is stored as
4856 DAG-CBOR blocks.
···5462Local SQLite files under `dev/` are ignored workspace state. Do not commit
5563database files, WAL files, `.env`, `.zig-cache`, `zig-out`, or package cache
5664directories.
6565+6666+## commands
6767+6868+The root `justfile` is the stable task surface:
6969+7070+```sh
7171+just test
7272+just smoke
7373+just invite https://pds.zat.dev
7474+just plc-repair did:plc:...
7575+just bench all
7676+just docker-publish-current
7777+```
7878+7979+Run `zig zen` before committing.
+16-3
docs/operations.md
···55Run these before committing:
6677```sh
88-zig build test
99-tools/smoke.sh
88+just test
99+just smoke
1010zig zen
1111```
12121313-`tools/smoke.sh` starts a local server, creates a session, writes records,
1313+`just smoke` starts a local server, creates a session, writes records,
1414checks blob upload, verifies repo/sync endpoints, and asserts that known app
1515records use valid key shapes.
1616···7272 including invite-code minting.
7373- `ZDS_INVITE_REQUIRED`: set to `true` to require invite codes for account
7474 creation. `describeServer` reflects this value.
7575+- Passkeys do not require a separate deployment secret. WebAuthn RP ID is
7676+ derived from the public URL host.
75777678PLC key lifecycle:
7779···126128127129Migration requires working public HTTPS, stable JWT secret, email delivery, blob
128130storage, and writable SQLite storage.
131131+132132+## conformance
133133+134134+Useful external checks:
135135+136136+- `https://check.cirrus.earth/?target=<handle>` for PDS and OAuth conformance.
137137+- bsky.app and pdsls.dev for browser-visible OAuth, proxy, repo, and blob
138138+ behavior.
139139+140140+Treat browser failures as request/response evidence: inspect the failing XRPC,
141141+status code, response body, and relevant server logs before changing behavior.
+46
docs/passkeys.md
···11+# passkeys
22+33+ZDS supports WebAuthn passkeys for OAuth login. This is modeled after
44+Tranquil's passkey support; passkeys are outside the official Bluesky PDS
55+surface ZDS tracks for compatibility.
66+77+## storage
88+99+Each passkey row belongs to an account DID and stores:
1010+1111+- credential ID
1212+- credential public key
1313+- signature counter
1414+- friendly name
1515+- created and last-used timestamps
1616+1717+Challenge rows are short-lived. Registration challenges are tied to the account
1818+DID. Discoverable login challenges are tied to the OAuth request so an
1919+authenticator can identify the account by credential ID.
2020+2121+## login behavior
2222+2323+If the login hint resolves to an account with saved passkeys, the OAuth page
2424+offers passkey login first and keeps password login available. If no passkey is
2525+saved for the account, password login is shown directly with a link to the
2626+security page.
2727+2828+Discoverable passkey login is supported: the browser may return a credential
2929+without the page first choosing an account. ZDS resolves the credential ID to
3030+the stored account, verifies the assertion, updates the signature counter, and
3131+authorizes the OAuth request.
3232+3333+## management
3434+3535+Passkeys are managed from `/security`. That page signs in with the account
3636+password, lists saved passkeys, and supports add, rename, and delete.
3737+3838+Deleting a passkey removes it from ZDS. The browser, OS, or password manager
3939+may still keep its copy, but it can no longer be used with this PDS.
4040+4141+## references
4242+4343+- Tranquil frontend: `frontend/src/routes/OAuthLogin.svelte`,
4444+ `frontend/src/routes/Settings.svelte`
4545+- Tranquil storage traits: `crates/tranquil-db-traits/src/user.rs`
4646+- ZDS WebAuthn dependency: <https://tangled.org/zzstoatzz.io/webauthn>
+3
docs/references.md
···4747- Invite codes are core PDS state. Captcha, 2FA, and migration-only policies can
4848 live at the PDS boundary or a reverse proxy boundary; pds-gatekeeper is useful
4949 prior art for that layer, not a substitute for invite-code accounting.
5050+- Passkeys are Tranquil-derived, not reference-PDS-derived: store credential
5151+ IDs, public keys, counters, names, and timestamps, and verify WebAuthn
5252+ assertions before authorizing OAuth requests.
50535154## Tranquil comparison
5255