Dependencies
DraftThis doc is the single owner of Capsule’s canonical implementation-dependency pins: for each domain concern, the one library that implements it, where the pin applies, and the bounded exceptions. Introducing a dependency for a domain not listed here requires adding a row here first; changing a pin is a one-doc edit plus a migration slice in the repo-root SLICES.md — never an in-place drift.
What this doc deliberately does not own, per the SSoT rule:
- Cryptographic primitives and their crates — Cryptography — Primitives.
- TLS version policy — Failure Modes — Transport Security. This doc pins the implementation (rustls); the policy lives there.
- Client test and performance tooling — Clients — Test and Performance Tooling.
- Which existing identifier uses which UUID version — Metadata — Identifiers. This doc owns only the default-for-new rule.
- Media formats and codecs — Thumbnails and Previews owns derivative formats and the LQIP contract (encoding tier, byte width, versioned
dominant_colorfallback); the sidecar’scontent_typeset is owned by Metadata. The libraries that implement those formats are still pinned here. - Which licences a dependency may carry, and how it may be linked — Licensing. This doc picks the library; that doc decides whether its licence and linkage are admissible, and
deny.tomlenforces the answer.
Mechanically, every Rust version is pinned once in the root Cargo.toml [workspace.dependencies]; member crates consume it with workspace = true and never declare their own version. Toolchain versions (Rust nightly, bun, tuist, …) are pinned by mise per CONTRIBUTING.md.
| Domain | Canonical choice | Scope | Exceptions |
|---|---|---|---|
| Datetime | jiff | All domain logic — parsing, formatting, arithmetic. Signed and wire formats carry RFC 3339 strings or integer epochs, never a datetime library type, so the pin never touches serialized bytes. | chrono remains only as the sea-orm column type in capsule-cli/entity, converted to jiff at the entity boundary, and in non-buildable review-only server code under legacy-review/. (The last buildable non-entity holdout, the frozen GraphQL crate with its async-graphql chrono scalars, was removed with slice S-G1.) |
| Error handling | thiserror in libraries; eyre + color-eyre in binaries | Libraries define typed error enums; binaries (CLI, server main, xtask) wrap them in reports. | anyhow is not used. |
| Logging | tracing (facade) + tracing-subscriber (binaries) | All crates; structured fields and hot-path spans per the traceability rule in AGENTS.md. The log facade is forbidden in new code. | Remaining log:: call sites in capsule-core / capsule-core-ffi migrate in slice S-F6. |
| TLS implementation | rustls (with tokio-rustls as the async adapter) | Wherever Capsule code holds a TLS stack: the SDK’s HTTP client, LAN-peering mutual TLS (tokio-rustls), server egress, sea-orm’s runtime-tokio-rustls. The ring provider is pinned for the peering stack so it never depends on an ambiguous process-default CryptoProvider. Never native-tls/openssl. | None. The one exception this row used to carry — openssl as a transitive dependency of webauthn-rs attestation-certificate verification — goes with passkeys (S-C56) and with the capsule-server tree that holds them. |
| X.509 leaf generation | rcgen | The per-connection self-signed leaf the LAN-peering mTLS handshake presents. The certificate carries no trust of its own — peering is CA-less and identity is decided by the application-layer hybrid check — so the leaf is ephemeral. ring provider, matching the rustls pin above. | Server-facing certificates are operator-provisioned, not minted in-process. |
| LAN service discovery | mocked seam (capsule-sdk::peering::Discovery) | Peering’s mDNS advertisement/browse is behind a trait seam; the opaque, rotating descriptor is pure and unit-tested. A live responder (pure-Rust mdns-sd) is the sanctioned implementation to plug in — added by a follow-up slice with its own row, since a live multicast responder is non-deterministic and untestable in CI. | — |
| Identifiers | uuid — UUIDv7 for every newly introduced identifier | Time-ordered v7 is the default (index locality); the assignment of existing ids is owned by Metadata — Identifiers. | UUIDv4 where an id must not leak creation time (e.g. device_id). Capability-bearing opaque ids (share links, drops) are not UUIDs at all — they carry their own ≥128-bit entropy per their owner docs. |
| Async runtime | tokio | All async code. | — |
| HTTP server | Kynos | All capsule-server REST/OpenAPI surfaces, including sync and federation. | No secondary public transport. |
| Kynos sourcing | kynos = { version = "0.1.0", features = ["openapi32"] } — from crates.io | Kynos published 0.1.0 on 2026-08-29, which discharges the repin-on-publish exit this row previously carried: the git dependency and its pinned rev are gone, so a bump is an ordinary reviewed version change rather than a rev audit. Tokio-only, MSRV 1.85, edition 2024. openapi32 is a strict, purely additive superset of the default openapi31. Enabling the feature does not by itself make the document 3.2: Router::openapi() emits the lowest version expressing the API without loss, deliberately not keyed on the feature, because Cargo unifies features across a dependency graph and a document’s version must not follow a flag an unrelated crate turned on. Capsule therefore pins the version explicitly with openapi_as(SpecVersion::V3_2) — the case Kynos names as “a consumer’s toolchain pins a version”, and one that targets rather than downgrades, so an unexpressible construct is an error naming what blocks it, never a document with operations quietly missing. | Master has moved past the 0.1.0 tag (512adbc7) — the delta is docs/CI plus Accept-Language negotiation, which Capsule does not adopt (error codes are localized client-side, offline). Repin at 0.2 if a released feature is needed. |
| HTTP body | http-body-util | capsule-server’s coded-problem interceptor (S-C36) only: it reads a rendered RFC 9457 body back before putting it down again, and Kynos’s Body is an http_body::Body with no inherent collector. Already in the lock file through Kynos and hyper, so it adds nothing to the tree. | Not a general HTTP abstraction: nothing else in Capsule touches a body outside a typed extractor, and a second use is a sign something is bypassing one. |
| HTTP client | reqwest (default-features = false, rustls-tls) | capsule-sdk — the sanctioned network path. | — |
| REST client codegen | spargen 0.4.0 (in-house, OpenAPI 3.1.x and 3.2.x) | capsule-sdk build-dependency only: build.rs lowers the committed capsule-sdk/openapi.json — emitted deterministically from the Kynos server’s own OpenAPI document — into the typed rest::Client, wrapped by client::AuthenticatedClient (slice S-D8). Its runtime support is embedded into the generated module, so spargen never enters the SDK’s runtime tree. Since 0.3.0 it enforces runtime dependency contracts, which set the floors on bytes, reqwest, serde and serde_json in the root manifest — bump those together or generation fails. Its API changed in 0.3: Config split into Spec/Build, and Report::outcome is a method (a Cached outcome is a success, not a failure). | Progenitor is gone. The two exclusions this row used to carry are lifted: object-typed query params and binary bodies both lower correctly as of 0.2.2, so the byte-serving surface is generated rather than hand-written. What stays hand-written is orchestration, not parsing — the resumable upload state machine (S-D1), token refresh, sync, recovery and protocol-version negotiation. Four Salvo-emitted operations are narrowed with spargen::omit! because they are structurally invalid; see the gates table in SLICES.md. |
| Second factor | totp-rs (otpauth, gen_secret) | The RFC 6238 codes of the local auth path’s second factor (slice S-C55), in capsule-server’s auth::totp alone. The parameters are Capsule’s and are published as constants — SHA-1, six digits, a thirty-second step, one step of drift — because an authenticator app assumes all four and a deployment that changed one would issue provisioning URIs that silently mis-generate. What the crate does not own is replay: a code is accepted at most once, and that is a compare-and-set in the enrollment store, not an algorithm. | The crate’s own skew is deliberately unused: Capsule walks the drift window itself because it needs to know which step matched, and check reports only that one did. |
| Constant-time comparison | subtle | The one place a secret-derived value is compared byte for byte: the second factor’s code check (S-C55). A hand-rolled fold is what an optimizer is free to short-circuit, and the resulting code looks correct forever. Already in the tree under aes-gcm, so this promotes a transitive dependency rather than adding one. | Password and manifest comparisons do not use it — a password never rises above its adapter, and signature verification is the signature crate’s own constant-time path. |
| WebAuthn | none — passkeys are not in v1 | Slice S-C56. webauthn-rs survives only inside the retiring capsule-server, and leaves the workspace with it. | This is why openssl leaves the tree. webauthn-attestation-ca was the sole edge pulling it in, for attestation-certificate parsing — never as a TLS stack, which is the carve-out the TLS row recorded. With passkeys deferred, the carve-out is spent and the TLS rule holds without exception. A rebuild reopens both. |
| ORM | sea-orm (sqlx-postgres on the server, sqlx-sqlite in the CLI) | The rebuildable index databases only — sidecars stay canonical per Principles. | — |
| Embedded SQLite | rusqlite (bundled) | capsule-core’s library.sqlite. | — |
| Vector index | sqlite-vec (vec0) | The client-local embedding index in capsule-core’s library.sqlite — per-task vec0 virtual tables under the embedding-provenance invariant. Optional + native-gated alongside rusqlite (registers as a SQLite auto-extension; not wasm32). | Server-side vector-DB idioms (pgvector/HNSW) do not apply — the index is client-local SQLite by design. |
| LQIP placeholder codec | chromahash 0.7.1 | capsule-core::lqip (slice S-B14) — the only encoder/decoder for the signed sidecar lqip field. Imported directly, never through Rawshift (AGENTS.md), and deliberately outside the retiring capsule-core::media stack so one implementation serves the import pipeline, the uniffi FFI, and capsule-wasm. The tier, byte width and versioned fallback are the contract at Thumbnails — LQIP; this row owns only the pin. The AGENTS.md gate that read “after its v1 release” is amended to 0.7.1 — the release the project accepts as ready — and xtask’s architecture check stopped forbidding the crate in 2f8beeb, because a check that forbids an approved dependency has stopped describing a decision and started blocking one. | thumbhash is retired, not excepted. The Rust crate behind capsule-core’s media feature and the npm package in capsule-web both go; thumbhash stays in the architecture check’s retired-dependency list so it cannot return. BlurHash was never adopted. |
| Free-space probe | rustix (Unix, fs) + windows-sys (Windows, Win32_Storage_FileSystem) | capsule-core::library::available_bytes — the streaming-import free-space probe (statvfs / GetDiskFreeSpaceEx). Host-only, behind the native feature; the wasm32 sealing build links neither. | — |
| Windows TPM (TBS) | windows-sys (Windows, Win32_System_TpmBaseServices) | capsule-core::crypto::keys::tbs — the Windows device-key HardwareSigner (slice S-F4). The raw TPM 2.0 command channel (Tbsi_Context_Create / Tbsip_Submit_Command) the tss-esapi reference (crypto::keys::tpm, Linux) wraps; links tbs.dll via raw-dylib, so no new crate — an extra feature on the existing windows-sys row. #[cfg(windows)]-gated; the pure wire codec + mock tests run on any host. | Not tss-esapi on Windows: TBS is native and avoids the libtss2/bindgen build. |
| MLS group layer | openmls (libcrux-provider) + openmls_libcrux_crypto + openmls_basic_credential + openmls_traits + openmls_memory_storage | capsule-core::crypto::authority::OpenMlsAuthority — the live RFC 9420 MLS backend (slices S-X1/S-X2), pinned to the X-Wing PQ ciphersuite MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519 (0x004D). The libcrux provider is the only released one shipping the X-Wing suite (formally-verified ML-KEM + X25519 from Cryspen). openmls_memory_storage (S-X2) is the OpenMLS storage provider Capsule owns — the libcrux provider bundles a private one, so pulling it in directly lets the authority serialize durable group state (export_state/import_state) via the provider’s public values keyspace. Same version the libcrux provider already resolves transitively. Host-only, behind the mls feature (implied by native); the wasm32 sealing build excludes it — libcrux does not target wasm32-unknown-unknown. The offline ReferenceAuthority stays available on every build. | Not mls-rs: no third-party audit and no PQ suite (see Cryptography — MLS). 0x004D is a private/experimental codepoint (no IANA number) — acceptable for Capsule’s closed deployment; a future move to a WG-standardized ML-KEM-hybrid suite rides the album upgrade ceremony (S-X3). |
| CBOR | ciborium (+ serde_bytes, half) | All CBOR; the canonical-encoding rules are owned by Metadata — Canonical CBOR Encoding. | — |
| Serialization | serde + serde_json | Derives and JSON surfaces. | — |
| CLI | clap (derive) | capsule-cli, xtask argument parsing where non-trivial. | — |
| Test runner | cargo-nextest; testcontainers (+ podman) for real backing services | The Unit/Smoke tiers per Principles — Validation Tiers. Binary-level smokes spawn the built binary through Cargo’s own CARGO_BIN_EXE_<name> from a tests/ target (capsule-cli/tests/cull_round_trip.rs, slice S-D16) — a real process boundary with no crate to add. | No assert_cmd/predicates: they would buy assertion sugar over std::process::Command, not capability, and the minimalism rule does not trade a dependency for sugar. |
| FFI bindings | uniffi | One workspace version — consolidation is slice S-F1. | — |
| Browser FFI (WASM) | wasm-bindgen (+ wasm-bindgen-cli, mise-pinned) | capsule-wasm — the guest web client’s share-link open surface (slice S-E1): parse the URL fragment secret, Argon2id passphrase unwrap, scope decapsulation, STREAM blob decrypt. Compiles capsule-core (--no-default-features: crypto + cbor + sharing) to wasm32-unknown-unknown; wasm-bindgen-cli (the build-wasm mise task, --target web) post-processes the cdylib into the browser JS glue. The CLI and crate are pinned to the same version (0.2.100) — wasm-bindgen requires it. Randomness rides the getrandom wasm_js backend already pinned in .cargo/config.toml. | Not wasm-pack: it fetches a version-matched wasm-bindgen at run time, breaking the offline gate; the cargo build + wasm-bindgen path uses only pre-installed toolchains. |
| Domain | Canonical choice | Notes |
|---|---|---|
| Framework | React 19 + rsbuild | — |
| Routing / data | TanStack Router / TanStack Query | — |
| Styling | Tailwind v4, class-based dark mode (@custom-variant dark + ThemeProvider) | — |
| Validation | zod | — |
| Datetime | none — native Intl / Date | A date library is added only by adding a row here. |
| LQIP decode | none — no JavaScript placeholder codec | The thumbhash package and the lazy-image.tsx decode path it fed are removed with the move to Chromahash. The browser has no decrypted lqip to render today — the authenticated read path is a key-free projection of the sync feed (API Surfaces) — and when it does, it decodes through the capsule-wasm module below over the same capsule-core::lqip code, not a second implementation in JS. |
| i18n runtime | FormatJS over the generated catalogs | Contract owned by Internationalization. |
| Guest share crypto (WASM) | capsule-wasm module (built by mise run build-wasm) | The /s/{opaque-id} viewer’s client-side share-link open path — the URL fragment secret and any passphrase never leave the browser (slice S-E1). Generated into the gitignored src/generated/wasm/; never committed. The Rust FFI crate + build tool are the Rust — Browser FFI row. |
| Lint/format | Biome | — |
| Runtime / package manager | bun | — |
Test frameworks and performance tooling are owned by Clients — Test and Performance Tooling. Bindings come from the single-version uniffi strategy (slice S-F1). Project generation and formatting (Tuist, SwiftLint, SwiftFormat) are mise-pinned toolchain, not library pins.
Kotlin
Section titled “Kotlin”Bindings likewise ride the uniffi strategy (S-F1); JNA loads the produced library. JUnit 5 is the current test harness; the canonical pin is recorded in Clients — Test and Performance Tooling when the Android harness stabilizes.
Validation
Section titled “Validation”The pins are enforced structurally, not by convention: cargo tree -i chrono -e no-dev must resolve to capsule-cli/entity and sea-orm internals only; cargo tree -i thumbhash must resolve to nothing and rg thumbhash capsule-web must be empty (the architecture check’s retired-dependency list holds the Rust half); rg 'log::' outside the S-F6 scope, and any openssl/native-tls edge outside webauthn-rs, are review-blocking. The per-platform mise run check-* gates run the pinned toolchains.