Skip to content

API Surfaces

Draft

Capsule’s server exposes exactly two wire transports, chosen per surface: REST with an OpenAPI schema for request/response surfaces, and gRPC for the sync feed and federation pull. This doc owns the surface ↔ transport map and the cross-transport mapping of the universal handshake and rejection semantics. The handshake rules stay owned by Threat Model — Validation; error-code identity stays owned by Internationalization; this doc owns only how both ride each transport.

SurfaceTransportCrateOwner doc
Authentication (sessions, passkeys, TOTP, OIDC)RESTcapsule-api-authAuthentication
Resumable upload (POST/HEAD/PATCH /upload)RESTcapsule-api-uploadImport — Upload Protocol
Lifecycle writes (POST /albums/{album_id}/ops)RESTcapsule-api-upload::ops (planned)Authorization
Blob fetch (GET /blob/{hash}, HTTP Range)RESTcapsule-api-mediaImport — Download & Sync
Share serving (/s/{opaque-id})RESTcapsule-api-media::shares (planned)Share Links
Guest drops (/u/{opaque-id}/drop, inbox, adoption)RESTcapsule-api-media::drops (planned)Web Upload
Storage verification (POST /storage/verify)RESTcapsule-api-media::verify (planned)Import — Storage Verification
Device enrollment (/auth/devices/enroll…)RESTcapsule-api-auth::devices (planned)Device Enrollment
Version/handshake surfaceRESTcapsule-apiThreat Model — Validation
Sync feed (change discovery after a cursor)gRPCcapsule.sync.v1.SyncServicecapsule-api-sync (stub today)Import — Download & Sync
Federation pull (peer server fetches an album’s feed)gRPC — same service, capability-gatedcapsule-api-sync::federation (planned)Federation
Library queries (timeline, albums, search)none — client-side over library.sqlitecapsule-core::library + dbOrganization

Two consequences worth stating plainly:

  • Blob bytes always ride REST. Ranged, resumable byte transfer is what HTTP is best at (Range, caches, proxies); gRPC has no ranged-read idiom. The gRPC surface carries only the small, typed feed — content hashes, envelopes, encrypted metadata blobs — never original or derivative bytes.
  • Rich queries have no server surface at all. The server is key-free and cannot evaluate a content predicate; every timeline, filter, and smart-album query runs client-side against the rebuildable local index. What the server offers is exactly the feed needed to build that index.
  • REST + OpenAPI for every request/response surface: the OpenAPI 3.1 schema is the machine-readable contract that generates capsule-sdk’s typed REST client via spargen, our in-house generator (in development; SLICES.md S-D8). Progenitor was dropped because it consumes OpenAPI 3.0 only, forcing a lossy 3.1→3.0 down-conversion — we do not downgrade schemas. A plain HTTP surface stays debuggable with nothing but curl — which matters for a self-hosted product.
  • gRPC only where a typed, paged feed contract earns it: the sync feed and its federation twin are one proto contract consumed by every client and every peer server, with the signed manifest traveling as opaque canonical CBOR inside it (never re-modeled as proto fields — re-encoding would detach it from its signatures).

capsule-api-library exposes an async-graphql schema at /v1/library. It predates the E2EE key-free server model and is retiring, not evolving:

  • Its resolvers presume 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 crate stays frozen (compiling, unmounted from new work) until the client-side query path reaches parity; retirement is an explicit slice in the repo-root SLICES.md. New surfaces MUST NOT be added to it.

The universal headers are defined once, as REST headers. On gRPC they ride call metadata with the lowercased key names; the values and fail-closed rules are identical.

REST headergRPC call metadataDirection
X-Capsule-Protocolx-capsule-protocolrequest
X-Capsule-Crypto-Suitex-capsule-crypto-suiterequest (writes)
X-Capsule-Sidecar-Schemax-capsule-sidecar-schemarequest
X-Capsule-Protocol-Minx-capsule-protocol-minresponse metadata
X-Capsule-Protocol-Maxx-capsule-protocol-maxresponse metadata
X-Capsule-Min-Client-Buildx-capsule-min-client-buildresponse metadata

Credentials map the same way: the REST Authorization: Bearer access token and the federation capability token both ride the gRPC authorization metadata key unchanged — one token format (EdDSA-JWT), two carriages.

REST rejections are HTTP statuses; gRPC rejections are gRPC status codes. The mapping is deliberately coarse — the precise discriminator on both transports is the machine-readable error.* code, carried in the REST ApiError body and in the gRPC status detail (plus the x-capsule-error-code trailing metadata).

Rejection classRESTgRPC
Structural (bad envelope, unknown enum, sizes)400INVALID_ARGUMENT
Unauthenticated / expired token401UNAUTHENTICATED
Unauthorized (capability, quota-hard, suspend)403PERMISSION_DENIED
Not found (and indistinguishable-404 surfaces)404NOT_FOUND
Stale state (chain/cursor/directory regress)409FAILED_PRECONDITION
Payload too large413RESOURCE_EXHAUSTED
Unsupported media type (chunk body)415INVALID_ARGUMENT
Protocol version outside [Min, Max]426FAILED_PRECONDITION
Rate limited429RESOURCE_EXHAUSTED

Where two rejection classes share a gRPC code (409/426, 413/429), the error.* code disambiguates — a client switches on the code, never on the transport status alone.

  • Handshake parity (unit). For each fail-closed rule in Protocol and Capability Negotiation, drive the same bad input through a REST route and the gRPC service; assert the rejection maps per the table above and carries the same error.* code.
  • Metadata round-trip (unit). Send the universal values as gRPC metadata; assert the server’s negotiation gate reads them identically to the REST headers.
  • Capability carriage (unit). Present the same federation capability as a REST bearer and as gRPC authorization metadata; assert identical verification outcomes.