Skip to content

API Surfaces

Draft

Capsule exposes one public server transport: Kynos REST with a checked-in OpenAPI 3.2 contract. This document owns the surface-to-module map, the document model that contract is served under, and the HTTP carriage of the universal handshake. Handshake rules remain owned by Threat Model — Validation, and stable error-code identity by Internationalization.

How these surfaces are published as developer reference — the artifact each is generated from and the gate that keeps it current — is Developer Documentation.

SurfaceTransportPlanned moduleOwner doc
Authentication (sessions, TOTP, OIDC)RESTcapsule-server::authAuthentication
Resumable upload (POST/HEAD/PATCH /upload)RESTcapsule-server::uploadUpload Protocol
Lifecycle writes (POST /albums/{album_id}/ops)RESTcapsule-server::upload::opsAuthorization
Blob fetch (GET /blob/{hash}, HTTP Range)RESTcapsule-server::blobDownload & Sync
Sync feed (change discovery after a cursor)RESTcapsule-server::syncDownload & Sync
Federation pullRESTcapsule-server::federationFederation
Share serving (/s/{opaque-id})RESTcapsule-server::shareShare Links
Guest drops (/u/{opaque-id}/drop, inbox, adoption)RESTcapsule-server::dropWeb Upload
Storage verification (POST /storage/verify)RESTcapsule-server::blobStorage Verification
Device enrollment (/auth/devices/enroll…)RESTcapsule-server::auth::devicesDevice Enrollment
Version/handshakeRESTcapsule-serverThreat Model — Validation
Library queries (timeline, albums, search)none — client-side over library.sqlitecapsule-core::library + dbOrganization

Blob bytes use ranged HTTP. The sync feed carries only small opaque envelopes, encrypted metadata, and blob references; clients fetch content separately with Range. Rich content queries have no server surface because the key-free server cannot evaluate them.

Status note (web client). The browser’s authenticated read path is a key-free projection of the sync feed (ids, membership, blob addresses); the Worker-hosted WASM decode/verify boundary that would fill decrypted titles/thumbnails is post-v1 (decision 2026-07-12). v1’s web surfaces are the guest drop and the share-link viewer.

The checked-in OpenAPI 3.2 document is the public contract and the input to Spargen. Kynos owns routing, middleware composition, and deterministic schema emission. Capsule owns authentication, protocol headers, error bodies, encrypted-upload state, blob storage ports, and business rules.

The Document Is 3.2, and Its Version Is Pinned

Section titled “The Document Is 3.2, and Its Version Is Pinned”

3.2 is not a floor the document may drift above; it is the version the document declares. capsule-server::openapi() calls router().openapi_as(SpecVersion::V3_2) rather than the plain router().openapi(), because the plain emitter returns the lowest version that expresses the API without loss — a sound default for a document produced on demand, and the wrong one for a contract that is committed to the repository and generated from. Left to follow the API, the committed contract would flip 3.1 → 3.2 the day the first streamed response landed, churning the schema gate and regenerating the SDK for a change nobody asked for.

Enabling Kynos’s openapi32 feature is a different thing and does not by itself produce a 3.2 document; why the emitted version is deliberately not keyed on a feature flag belongs to the Kynos row in Dependencies. What matters here is the consequence: openapi_as targets rather than downgrades, so a construct 3.2 cannot express is an error naming what blocks it, never a document with operations quietly missing.

The line is parsing and serialization versus orchestration, and with Spargen’s two gaps closed it now falls in exactly one place.

Generated from the contract: every request and response body, every typed parameter, and the byte-serving endpoints. The blob-fetch and asset-serve tree — GET /blob/{hash} with Range, and the derivative reads beside it — is back in the generated client, because textual and binary response decoding and typed parameter serialization both work now. A hand-written byte path is no longer justified by a generator limitation, and adding one back would be a second parser for a surface the contract already describes.

Hand-written: the resumable upload state machine (slice S-D1) — chunk scheduling, offset resumption after an interruption, retry laddering, and the connection-class budget. None of that is parsing. It is orchestration over the generated calls, driven by conditions an OpenAPI document cannot express, which is why it was never a generator gap and does not close with one. This is the existing contract with the gaps removed, not a new one; the Spargen pin and its history are the codegen row in Dependencies.

One transport keeps negotiation, observability, error handling, streaming, cancellation, and test harnesses consistent. GraphQL and gRPC are retired architecture, not compatibility surfaces. Review-only implementations under legacy-review/ may inform contract tests but cannot be mounted or restored directly.

The capsule.sync.v1 gRPC feed and the gRPC-web framing that served it to the browser retired with the Salvo server: sync and federation pull are REST operations on the same OpenAPI contract, with the signed manifest still travelling as opaque canonical CBOR (never re-modelled as wire fields — re-encoding would detach it from its signatures).

The browser followed (slice S-C60). capsule-web spoke gRPC-web through a hand-rolled Protobuf codec — 410 lines of varint and length-delimited framing, written to avoid pulling a protobuf-es/connect-web toolchain in to read a handful of fields — and now issues one GET /v1/sync and parses JSON. The store’s client rules are untouched: forward-version rejection and per-album anti-rewind were always behind a transport seam, which is what a seam is for.

Two things the browser can no longer see, and both are the REST feed being stricter: a blob’s MIME type, which was plaintext metadata about an encrypted blob and is simply not a field any more, and an “unspecified” change kind, which existed only because a proto3 enum defaults to zero when a field is absent.

A now-deleted crate exposed an async-graphql schema at /v1/library. It predated the E2EE key-free server model and was removed in slice S-G1 (repo-root SLICES.md); no GraphQL surface exists. It was never evolving, for reasons that also foreclose reviving it:

  • Its resolvers presumed a server that can read content (people, faces, smart tags, memories server-side) — structurally impossible under the threat model; the key-free replacements are client-side ML (AI/ML) and client-side views (Organization).
  • The generated SDK is OpenAPI-derived and cannot drive GraphQL, so the surface was never consumable by the client stack the design commits to.
  • The query role it aimed at is served client-side over library.sqlite (above), fed by the sync feed — the parity precondition that unblocked retirement.

Every public route applies the same headers:

HeaderDirection
X-Capsule-Protocolrequest
X-Capsule-Crypto-Suiterequest for writes
X-Capsule-Sidecar-Schemarequest
X-Capsule-Protocol-Minresponse
X-Capsule-Protocol-Maxresponse
X-Capsule-Min-Client-Buildresponse

Credentials use Authorization: Bearer. Session access tokens and federation capabilities are different token types verified by their owning modules, even though both use the standard HTTP carriage.

REST status is coarse; the stable error.* code in the ApiError body is the precise discriminator. Clients switch on the code, never on status alone.

Rejection classHTTP status
Structural (bad envelope, unknown enum, sizes)400
Unauthenticated or expired token401
Unauthorized (capability, quota-hard, suspension)403
Not found, including indistinguishable-404 surfaces404
Stale state (chain, cursor, directory regression)409
Payload too large413
Unsupported chunk media type415
Protocol outside [Min, Max]426
Rate limited429
  • Drive every fail-closed handshake rule through representative routes in each module and assert the same headers, status, and error.* code.
  • Generate the OpenAPI contract twice and assert byte-identical output, and assert the emitted document declares OpenAPI 3.2 — a contract that silently reverted to 3.1 would still be valid, still generate, and no longer be the committed decision. Generate the Spargen client from the checked-in contract and fail CI on drift, byte-serving operations included.
  • Exercise streaming upload and ranged download with cancellation, backpressure, retry, and body limits without live infrastructure.
  • Present valid and invalid federation capabilities through REST and assert compartmentalized, indistinguishable rejection behavior.