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.
3. Link operations¶
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.
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 clientMutationId → RuntimeMutationId 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):
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. |