Skip to content

Surface contract (L1)

1. Boundary

The renderer reaches mail behavior through one runtime adapter over the authority server handle (runtime L1 §4). The UI sends typed scopes and named actions and receives runtime-produced state; it never owns query truth, mutation settlement, or provider access. This sub-domain is that surface: the handle the adapter wraps, the operations it exposes, and the frames it streams back. The data shapes those operations carry and the flow that produces them are adapter L2; the mutation catalog is mutations L1.

1.1 Authority server handle

One authority server handle, independent of Axum and Tauri transport details, owns or references the service, store, supervisor, view registry, mutation coordinator, event bus, and body/blob cache. Transport code calls the handle; mail behavior is never implemented separately in the Axum router and the Tauri adapter.

The /v1 router is an adapter over the same handle (useful for MCP, scripts, tests, debugging, external clients, and migration work); it may be mounted on a loopback listener in bundled mode, but renderer components do not depend on route handlers when a runtime adapter operation exists.

1.2 Renderer runtime adapter

The renderer runtime adapter maps typed UI operations to handle calls and streams runtime state changes back to webviews. Push delivery uses one link-scoped runtime stream carrying RuntimeFrame; commands (open/close views, run mutations) stay request/response operations returning identifiers or receipts. The contract is semantic, not a mandate for exact Tauri command names, so desktop IPC carries the same frames without exposing HTTP/SSE details to components.

2. Adapter identifiers

Runtime adapter payloads use opaque identifiers:

Identifier Owner Purpose
RuntimeLinkId Runtime One renderer connection.
ViewId Runtime One runtime-owned view opened by a link.
ViewRevision Runtime Monotonic revision for one view.
SubscriptionId Runtime or adapter One active state stream subscription.
ClientMutationId Renderer Stable idempotency key for a user-submitted mutation.
RuntimeMutationId Runtime Runtime settlement identity for an accepted mutation.

The renderer may store these for the lifetime of a window or component tree. It must not infer account, message, provider, or storage facts from their format.

A renderer link starts before it opens mail views:

openRuntimeLink(init?) -> RuntimeLinkConnection
subscribeRuntimeFrames(linkId, afterSeq?) -> RuntimeFrame stream
closeRuntimeLink(linkId)

init may include the window label, app version/build metadata, an account/source scope, and renderer capabilities (whether the window can open focused surfaces or download files). Startup status, account status summaries, and adapter capability flags then arrive as link-scoped RuntimeFrame values.

subscribeRuntimeFrames carries the unified link stream. Each frame has one monotonic RuntimeLinkSeq for the link; view snapshots/replacements, view errors, view closure, mutation settlement, notifications, and heartbeats share that sequence so the renderer has one resume cursor. If afterSeq is absent, unknown, or too old, the runtime collapses active views to current snapshots rather than replaying stale per-view deltas.

RuntimeFrame is the only renderer-facing server-to-client envelope:

RuntimeFrame =
  ViewSnapshot { linkSeq, viewId, revision, snapshot }
| ViewReplace { linkSeq, viewId, revision, snapshot }
| ViewError { linkSeq, viewId, error }
| ViewClosed { linkSeq, viewId }
| MutationSettlement { linkSeq, mutationId, state }
| Notification { linkSeq, kind, payload }
| Heartbeat { linkSeq }

The renderer demultiplexes by viewId, mutationId, or link-global notification kind inside the frame. It does not open /v1/events or a per-view stream for normal runtime behavior.

4. View operations

The renderer opens views with serializable descriptors inside a link:

openLinkView(linkId, descriptor, options?) -> ViewSnapshot
updateLinkView(linkId, viewId, descriptor | windowRequest) -> ViewSnapshot
closeLinkView(linkId, viewId)

openLinkView returns the initial or loading snapshot; later state arrives on the link stream as ViewSnapshot/ViewReplace, not a separate event feed. Ordinary updates are replacement frames, not renderer-applied patches. updateView covers query-text, sort, window-extension, focused-message, and settings-target changes, preserving the ViewId only when the runtime can treat the change as the same logical view.

4.1 View descriptors

The bundled adapter supports these descriptor families:

Descriptor Required identity Runtime-owned state
mailList QueryScope, projection kind, sort/window request Message/conversation rows, cursors, pending row markers, loading/error affordances.
messageDetail account/source id and message id Detail projection, body alternatives, attachments, body/attachment token state.
conversation conversation id and optional token Conversation envelope or view for the requested token.
compose draft id or compose intent Draft state, recipients, body, attachments, send readiness, save/send pending state.
settings settings surface descriptor Settings/account/smart-mailbox/source-mailbox state and edit settlement.
accountStatus account id or all accounts Folded account overview(s): config joined with runtime status, sync progress, provider errors, queued work. The all-accounts variant serves the account list.

A descriptor is declarative: components describe the state they need; they do not pass partially loaded messages for the runtime to repair.

5. Mutation submission and settlement

Mutations use one request shape; the catalog is mutations L1.

runMutation(linkId, operation, clientMutationId, context?) -> MutationReceipt

operation is the typed MailOperation — the one operation vocabulary, defined in posthaste-contract-core and parsed once at the wire; no stringly name/args pair crosses the adapter. clientMutationId is renderer-generated and reused on retry of the same intent. context may include the originating ViewId, expected ViewRevision, selected rows, or command source for diagnostics, but correctness cannot depend on private component state. The receipt means the runtime accepted or rejected the submission; observable mail changes arrive as view frames and settlement frames. When the runtime accepts a mutation it persists the clientMutationIdRuntimeMutationId mapping before applying local effects or sending provider commands, so a retry after reload or restart returns the existing record.

Settlement is reported with RuntimeFrame::MutationSettlement states: accepted, confirmed, failed. confirmed and failed are the terminal states; not every mutation dwells in accepted observably. The UI renders settlement state but keeps no pending set or replay log. Settlement frames are delivered to the submitting link and to links with active views that reference the RuntimeMutationId.

6. Resource operations

Attachment bytes, message source downloads, and other binary resources are runtime resources, not provider URLs exposed to the renderer. The adapter may return bytes directly, a Tauri resource handle, or a short-lived local capability URL. Capability URLs are scoped to the local link, expire, and carry no provider credentials.

7. Adapter error shape

Adapter calls and frames use a stable error envelope, defined in posthaste-contract-core (the shared wire vocabulary):

RuntimeAdapterError { code, message, retryable, correlationId, details }

Initial codes: runtime_not_ready, invalid_descriptor, invalid_mutation, unauthorized, not_found, provider_unavailable, conflict, transport_disconnected, internal. message is safe for UI display only when the runtime marks it so in details or the code definition allows it.

8. Assertions

ID Sev. Assertion
renderer-adapter-facade MUST Renderer components call one runtime adapter facade for mail state and named mutations.
renderer-one-frame-stream MUST Renderer push state uses one link-scoped RuntimeFrame stream with one RuntimeLinkSeq cursor.
renderer-no-events-cache-path MUST Renderer state delivery does not depend on /v1/events; integration feeds are separate API-platform surfaces.
opaque-runtime-identifiers MUST Runtime adapter identifiers are opaque and are not parsed for account, message, provider, or storage facts.
adapter-view-descriptors MUST Renderer views are opened with serializable descriptors rather than parent-owned partial mail state.
view-subscriptions-full-state MUST View subscriptions can deliver full replacement snapshots for the current view revision.
adapter-mutation-receipts MUST Mutation submission returns a runtime receipt and reports observable progress through settlement frames and view updates.
durable-mutation-idempotency MUST The runtime persists accepted client-mutation id mappings before local effects or provider commands.
repeated-client-mutation-id MUST Reusing a client mutation id with the same operation returns the existing mutation record or result; reusing it with a different operation payload is rejected.
settlement-frame-scope MUST Mutation settlement frames are routed to the submitting link and links whose active views reference the runtime mutation.
adapter-resource-boundary MUST Binary mail resources cross through runtime resource operations or scoped capability URLs, not provider URLs exposed to the renderer.
adapter-error-envelope MUST Adapter calls and frames use the stable RuntimeAdapterError envelope.
api-is-runtime-adapter MUST The /v1 router adapts the authority server handle instead of owning separate mail behavior.
tauri-is-runtime-adapter MUST The Tauri renderer adapter calls the authority server handle for links, views, mutations, resources, and events.