State contract (L1)
1. Boundary¶
1.1 Contract¶
The client domain defines the UI renderer that presents runtime state and sends user actions to the runtime.
The renderer does not own authoritative mail state, mail query evaluation, provider access, mutation settlement, active query-window repair, or durable body/blob cache state.
The renderer is distinct from the client-layer link node. When the runtime is remote, the user's device also runs a client-layer node — a replica runtime that holds a pending set, optimistic state, and a cache, and converges with the remote authority by confirmation watermark (replication L1, runtime L1 §2.3). That node is a runtime, not part of this domain. "The client holds local optimistic state" is true of the client-layer node; "the renderer holds no authority" is true of the view. Both hold at once.
1.2 Motivation¶
The UI should feel instant while staying simple. That requires a stateful runtime behind the renderer. The renderer can focus on layout, interaction, accessibility, and user feedback while the runtime owns mail state operation.
1.3 Reference models¶
TanStack Query remains a useful reference for keyed asynchronous state, and TanStack/query may still be used for non-mail or adapter-level caching. It must not become the authority for mail invalidation.
SWR and HTTP stale-while-revalidate are useful UI references for showing already available state while the runtime produces a newer value.
Relay runtime architecture and Apollo Client's normalized cache are references for separating entity-shaped state from rendered views. Posthaste does not adopt GraphQL.
Replicache is reference material for optimistic mutators and watermark reconciliation. Posthaste realizes that model in the runtime/replica layer as coherent links (replication L1); the renderer consumes their served state rather than implementing optimism itself.
Long message lists should follow a virtualized/windowed rendering model. TanStack Virtual and TanStack/virtual are the reference for this UI constraint.
2. Renderer boundary¶
2.1 UI-owned state¶
The UI owns presentation and interaction state:
- selected surface and focus
- pane sizes and layout preferences
- scroll anchors and viewport position
- open windows and overlays
- local text input state before submission
- animation and transient affordance state
- non-secret connection-profile metadata when the desktop client supports profiles
2.2 Runtime-owned state¶
The runtime owns mail and app state that affects durable behavior:
- active mail query windows
- message summaries and details
- conversation envelopes and views
- body and attachment freshness
- account, mailbox, smart-mailbox, settings, and sync state
- mutation pending/settlement state
- event ordering and recovery
The UI subscribes to these states through runtime views or runtime-provided stores.
Provider secrets, bearer tokens, message bodies, attachment bytes, runtime event history, and mutation idempotency records are not UI-owned state.
2.3 No direct provider or store access¶
The UI never talks to providers, SQLite, or JMAP/IMAP/SMTP directly. It also does not call origin HTTP query endpoints as the primary mail state path in bundled application mode.
3. Runtime adapter¶
3.1 Typed operations¶
The UI uses a runtime adapter with typed operations for opening views, subscribing to view state, fetching focused detail state, and running named mutations.
In bundled mode, the adapter is backed by the embedded authority server. The adapter hides whether transport is in-process, IPC-backed, a contained localhost bridge, or later implemented by a local replica.
3.2 Subscriptions¶
Views are subscription-like from the renderer's perspective. A component requests a mailbox, search, conversation, detail, compose, or settings view and renders the latest runtime state for that view.
Components do not repair query windows after mail events. The runtime streams replacement state when state changes affect the view.
4. Runtime-backed views¶
4.1 Mail lists¶
Mailbox, smart-mailbox, and search lists are rendered from runtime-owned query windows. The runtime owns membership, ordering, replacement rows, and cursors.
The UI may preserve scroll anchors and selection across runtime window updates, but it does not decide the mail list contents.
4.2 Message detail and conversations¶
Message detail and conversation views are rendered from runtime state keyed by message identity, conversation ID, and freshness tokens. If body, attachment, or conversation tokens change, the runtime refreshes the affected view state.
The UI may show loading, pending, or stale affordances supplied by the runtime, but it does not decide freshness by comparing mail tokens itself.
4.3 Settings and account views¶
Settings, account, mailbox, and smart-mailbox editing views render runtime-provided state and submit named mutations. Runtime state settles edits and publishes updates to other open windows.
5. Actions and mutations¶
5.1 Instant local phase¶
Regular actions should produce visible local runtime effects without waiting for provider or remote-origin round trips when the mutation contract permits it.
Examples:
- mark read/unread
- flag/unflag
- add/remove tag
- move/archive/trash
- open Inbox or Archive
- edit draft body or attachments
5.2 Mutation submission¶
The UI submits named mutations with stable client mutation IDs. The runtime returns pending, confirmed, or failed state through the same runtime state channel used for rendering.
The UI may present undo and redo from the client-navigated history (reconciled against the authority server's RevLog). An undo is an ordinary message.applyDiff carrying the inverse diff, not a renderer-side rollback engine and not an opaque undo mutation.
The renderer does not maintain its own mail mutation pending set or optimistic projection: accepted mutation idempotency, the pending set, and settlement are runtime-node state. When the runtime is remote that pending set lives in the client-layer replica node, not in renderer view state. The renderer renders the runtime node's served optimistic state and pending markers; it does not run a second, independent optimism engine that races the served state.
5.3 Multi-window behavior¶
Multiple renderer windows share one runtime. A mutation from one window updates other windows through runtime state updates.
6. Assertions¶
| ID | Sev. | Assertion |
|---|---|---|
| client-renderer-only | MUST | The client renders runtime state and does not own authoritative mail query, mutation, provider, or store logic. |
| client-presentation-state | MUST | Client-owned state is presentation or interaction state, not durable mail state. |
| client-no-secret-storage | MUST | Client-owned state does not include provider secrets, bearer tokens, message bodies, attachment bytes, event history, or mutation idempotency records. |
| runtime-adapter-opaque | MUST | UI code is opaque to whether the runtime adapter is in-process, IPC, contained localhost, or future replica-backed. |
| no-ui-query-repair | MUST | UI components do not repair mail query windows or decide authoritative list membership. |
| instant-regular-actions | MUST | Regular actions produce their local runtime effect without provider or remote-origin round trips when their mutation contract permits it. |
| named-mutation-submission | MUST | UI-submitted mail mutations use named mutation operations with stable client mutation IDs. |
| renderer-no-pending-set | MUST | The renderer maintains no mail mutation pending set or independent optimistic projection; the pending set and optimistic state belong to its runtime node (embedded authority, or a client-layer replica when the runtime is remote). |
| renderer-single-optimism | MUST | The renderer renders the runtime node's served optimistic state and pending markers; it does not run a second optimism engine that races served state. |
| multi-window-runtime-state | MUST | Multiple renderer windows observe shared runtime state rather than separate mail caches. |