Chris’s PDS implementation
cpds docs architecture.md
8.7 kB · 146 lines
Markdown
at main

Architecture #

For a first walkthrough, read the repository guide. This document records the design boundaries; README owns operating instructions, and production-plan.md owns unfinished production and federation work.

Two packages, one server #

The Cargo workspace contains the cpds server package and the small cpds-protocol library package. The server depends on the protocol package, never the reverse. Its API validates record shape, turns JSON $link/$bytes forms into native DAG-CBOR values, and computes content identifiers. It owns no HTTP, account, SQLite, filesystem, or network policy and can be tested independently.

The rest is organized into modules, not a crate per layer. Record writes, repository blocks, blob-reference indexes, account state, and durable events must share transaction rules. Artificially independent storage or generic CRUD packages would obscure that coupling rather than remove it. Small modules keep responsibilities readable without making every helper a public package API.

Assembly and adapters #

AppState::assemble wires services to a concrete Database and the narrow BlobStore, EventSink, and ClientRegistry interfaces. Services own authorization and orchestration; HTTP handlers translate requests and errors; persistence modules own SQL and transaction boundaries. Database code does not call HTTP handlers, and blob adapters do not decide OAuth or record visibility.

The ordinary HTTP test harness calls this same assembly function with real in-memory SQLite, MemoryBlobStore, a static OAuth client, and RecordingEventSink. Separate filesystem, process, and local fake-server tests exercise behavior that an in-memory router cannot prove.

BlobStore has four operations: check, put, get, and delete. Bytes are namespaced by owner and CID; MIME type, size, publication state, and references remain in SQLite. The shared contract test covers both bundled stores. EventSink has check and publish; it is a best-effort application hook, not the federation log.

Public repository transactions #

repository/records.rs validates identifiers, ownership, swaps, record data, and uploaded blob references. Typed mutations retain create/update/delete intent throughout ordered batch planning. repository/commit.rs applies them through ATrium's MST and stages content-addressed blocks before asking database/repo.rs to publish them atomically. The transaction-local block overlay and CID-verified reads live separately in database/blockstore.rs; that adapter stages bytes but does not decide which record changes may commit.

One successful public mutation transaction contains the final signed head, record index, blob-reference index, repository blocks, ordered operations, and durable sync-event metadata. An applyWrites batch produces one signed commit, not a series of hidden intermediate commits. The event proof contains newly written blocks still reachable from the final root. Compare-and-swap checks prevent a concurrent head or record change from being silently overwritten.

ATrium owns standard MST and repository machinery. CPDS owns staging, signing orchestration, validation, resource limits, and the SQLite commit. Opening an ATrium repository alone is not verification: CPDS also checks signatures, content hashes, revision metadata, and agreement with its SQL record index.

repository/export.rs exports verified local repositories as bounded CAR archives. Full exports, incremental exports, record proofs, and requested-block exports have different selection rules; a shared block table is not permission to return arbitrary known blocks. Remote CAR ingestion remains outside the implemented public surface.

Public data and Spaces are different stores #

Public records and alpha Space records have separate tables, authorization, revision histories, and commit formats. They share only appropriate mechanisms: canonical record encoding, blob bytes/metadata, identifier checks, identity resolution, and cryptographic dependencies. A Space is not a public repository with an extra access flag.

space/commit.rs implements the alpha LtHash-based deniable commit, while public repositories use repository-v3 signed MST commits. Space CAR recovery carries its signed view and index as two roots. These formats are deliberately not forced through one generic repository abstraction.

Blob visibility follows references. A public download requires a current public record reference. A Space download first requires authorization for the selected Space repository and a matching space_record_blobs index entry. Merely knowing a CID or uploading bytes grants neither public visibility nor Space access. Space responses and errors are no-store; only successful public blob and well-known responses receive the public cache policy.

Identity and authentication #

IdentityService centralizes handle and DID resolution. ATrium provides protocol models and resolver building blocks; CPDS retains DNS conflict handling, did:web path/port mapping, transport policy, document-ID checks, and the required handle backlink. Local startup uses the configured development identity. Production requires bidirectional network verification before listening and periodically revalidates it. A confirmed mismatch stores handle.invalid with an identity event; inconclusive transport failures preserve the last confirmed state.

OAuthService implements authorization codes with exact registered redirects, S256 PKCE, Argon2 account authentication, and hashed opaque credentials. Redeeming the code and inserting the access token is one SQLite transaction: failed token storage must not consume the code. The static registry is intentional; this is not the complete AT Protocol OAuth server profile.

Space delegation and DPoP-bound credentials belong to space/auth.rs, service-token/notification transport to auth/service.rs, and client attestations to auth/attestation.rs. Shared outbound checks live in identity/http.rs. Space DPoP support must not be confused with unimplemented OAuth DPoP support. The configured public origin, not forwarding headers, determines request-URL bindings.

Durability, replay, and recovery #

repository/stream.rs rebuilds retained SQLite events into generated ATrium messages. http/subscription.rs owns WebSocket framing, admission, backpressure, and shutdown. A disconnected consumer resumes from its last durable sequence cursor. A cursor older than retention receives OutdatedCursor and requires repository resynchronization. Failure of the optional event sink never turns an already committed write into an apparent failure; doing so would encourage unsafe retries.

repository/maintenance.rs separates bounded readiness checks and garbage-collection passes from exhaustive startup storage verification. Blob publication reserves metadata before durable byte publication. Collection durably claims aged unreferenced objects before deletion and considers both public and Space references. Block collection preserves current repository graphs and blocks required by retained events.

The process has five explicit modes: serve, verify, backup, request-crawl, and set-account-status. Verification and operator modes require existing storage and do not listen. Backup holds an SQLite write barrier across a native database snapshot and CID-verified blob copy; its manifest omits passwords and signing keys. Crawl requests deliberately do not open storage. The reversible deleted hosting state suppresses access; it does not erase stored bytes or backups.

SQLite currently uses one mutex-protected connection. This makes transaction ownership explicit but serializes database work. Synchronous SQLite and password hashing also remain blocking work; async function signatures and HTTP deadlines do not by themselves preempt them. Do not infer throughput or isolation from the router's concurrency limits. That tradeoff and the remaining operational gates stay visible in the production plan.