Skip to content

Patterns (L3)

The runtime contract crates (posthaste-contract-core, posthaste-runtime-api, posthaste-client-link) are separate from the node crates: the near node lives in posthaste-runtime and the far node in posthaste-authority-server (crate topology). The wrapping of the legacy API path behind the handle is complete and its dead residue removed (RFC-L2-architecture-cleanup D20); the surviving service-graph handle and from_api_bridge_* entry points are deliberate test-harness seams (testkit), not part of the durable contract.

1. Contract crate types

posthaste-contract-core holds the shared wire vocabulary and is wasm-pure: allowed dependencies are posthaste-domain-model, posthaste-replica-core, serde/serde_json, thiserror, and optional utoipa behind the openapi feature — no tokio, reqwest, rusqlite, mail-parser, or axum. The contract traits live in posthaste-runtime-api (typed, wire-free domain RPC returning serde domain types) and posthaste-client-link (link ops: mutation forward, RuntimeFrame stream, link/view-stream ops); they add async-trait and futures-util. The former RuntimeCore god-trait splits across these two traits. The mutation forward keeps its shape (named forward_mutation — one up-channel verb across both seams, RFC D35b):

async fn forward_mutation(&self, caller: RuntimeCaller, request: MutationRequest)
    -> Result<MutationReceipt, RuntimeError>;

where MutationRequest carries the typed MailOperation (parsed once at the wire, dispatched by exhaustive match), not a stringly name/args pair.

Runtime identifiers are opaque #[serde(transparent)] newtypes over strings (RuntimeLinkId, ViewId, SubscriptionId, ClientMutationId, RuntimeMutationId), defined in posthaste-contract-core; they expose new/as_str and no parsing that infers account, provider, mailbox, message, or storage facts. Revisions are numeric ViewRevision/RuntimeLinkSeq newtypes. Contract payloads serialize camelCase. Build-time failures use typed RuntimeBuildError; runtime-call failures use the adapter-safe RuntimeError(RuntimeAdapterError) — never a transport type.

2. Authority build order

build_authority_runtime(RuntimeBuildConfig) accepts pre-resolved config_root/state_root/cache_root and assembles in this order:

  1. create state and cache roots
  2. open the TOML config repository (init defaults/smart mailboxes, or import bootstrap_path, when empty)
  3. open the SQLite store at state_root/mail.sqlite
  4. build MailService and sync source projections
  5. create the runtime event broadcast channel
  6. build and start the authority-server AccountSupervisor
  7. build RuntimeHandle, RuntimeShutdownHandle, and initial RuntimeStatus

The builder binds no socket, builds no router, creates no webview, and reads no renderer state. Production uses the default SystemSecretStore; tests inject a mock SecretStore.

3. Runtime status and the liveness gate

RuntimeStatus reports only adapter-safe readiness: lifecycle state, config/store/cache readiness booleans, and configured account count — never filesystem roots, provider secrets, bearer tokens, SQLite handles, or keyring references. runtime_status stays callable after shutdown and reports Stopped; every other contract method (runtime-api and client-link alike) checks lifecycle liveness first, allowing Ready/Degraded and rejecting Starting/Stopping/Stopped with RuntimeNotReady. A separate mutation-service-availability gate distinguishes "runtime stopped" from "mutation runtime not wired".

4. Account, settings, and OAuth contracts

Account CRUD/lifecycle/reload/delete and settings/smart-mailbox mutations use transport-neutral request/result structs in posthaste-contract-core (the mutation_args families; shared domain-model types, but no Axum extractors or HTTP-status concepts); posthaste-authority-server owns the side effects: config/service writes, secret-store writes, automation backfill checks, and event append/broadcast. Account delete methods own the full resource linkage — managed secret deletion, supervisor removal, config deletion, and the account-delete event — while adapters handle only transport-owned assets (e.g. deleting an uploaded logo file after runtime deletion succeeds).

OAuth keeps HTTP concerns (validation, pending-flow state, authorization-code exchange, redirect/HTML) in posthaste-server; after token exchange the adapter calls handle methods to encode token sets, write managed secrets, create provider-first IMAP/SMTP OAuth accounts or patch existing accounts to OAuth2, publish account events, and drive supervisor lifecycle.

5. Provider reads and event subscription

Provider-backed reads use the authority-server LiveAccountRuntimeProvider seam and the authority supervisor, not route-owned gateway lookup. Mail-list/search reads use query_mail_page with MailQueryRequest { query, presentation, visibility }; the query string is the universal selector (in:Inbox → saved smart mailbox, in:Account/Archive → concrete source mailbox, conversation:<id> → conversation members). The runtime owns query parsing/evaluation, saved-query expansion, cache-maintenance triggers, and event publication; adapters only shape the transport response.

subscribe_events owns the durable replay query and the live broadcast receiver: it subscribes before querying backlog, returns RuntimeEventSubscription { replay, live }, filters both with EventFilter, and suppresses live sequences already covered by replay. Adapters map each DomainEvent to their wire format and never own replay/subscription logic.

6. Security hardening

A loopback bridge in bundled mode is a local capability channel, not the authority boundary. When present it: binds loopback/local-only; uses an unguessable process-local token required in an authorization header (query-token auth rejected); validates Host before route exemptions; validates origin/referer when present; restricts CORS to the packaged app and configured dev origins; keeps tokens in memory and writes no daemon port/token file in bundled mode; and serves no provider secrets to JavaScript.

The desktop host exposes only the named Tauri commands the renderer needs — no generic filesystem, shell, process, network, or environment access — and every command validates input with deny-unknown-field semantics, treating descriptors, identifiers, and URL parameters as untrusted. External URLs open only for allowed schemes (http/https) in the system browser; untrusted in-app navigation is blocked; email body content stays sandboxed/sanitized. Logs and diagnostics exclude secrets, tokens, capability URLs, Authorization headers, sensitive query strings, bodies, and attachment bytes. Resource handles/capability URLs are link-scoped, expire, and cannot be broadened by editing path/query. Account deletion and cache cleanup run as runtime mutations/maintenance; renderer storage cleanup requests runtime cleanup rather than deleting runtime roots directly.

7. Validation

Tests cover that the bundled app starts without a separate authority server; that the shared contract compiles without Axum/Tauri/React types and the handle builds without binding a listener, with both adapters calling the same contract; that provider secrets and client remote-profile tokens are not persisted in renderer-owned files, profile JSON, localStorage, logs, URLs, or packaged JavaScript; and that loopback tokens are header/capability-only, host validation precedes route exemptions, query-token auth is rejected, Tauri input rejects unknown fields, external URL schemes are restricted, and generic filesystem/shell capabilities are not exposed.

8. Assertions

ID Sev. Assertion
authority-build-order MUST Authority server assembly opens config, store, service, projections, event channel, account supervisor, handle, and shutdown in the documented order.
no-adapter-startup-in-builder MUST The authority server builder does not bind sockets, build routers, create webviews, or read renderer state.
status-no-sensitive-fields MUST Runtime status does not expose filesystem roots, provider secrets, bearer tokens, SQLite handles, or keyring references.
lifecycle-gate-before-operations MUST runtime_status remains callable after shutdown; all other operations check lifecycle liveness and return RuntimeNotReady outside Ready/Degraded.
account-mutation-contract-pattern MUST Account mutation contracts stay transport-neutral and concrete side effects live in authority server methods.
account-resource-linkage-runtime-owned MUST Authority server delete methods own managed secret deletion, account runtime removal, config deletion, and account-delete event publication.
oauth-materialization-runtime-owned MUST OAuth callback token-set persistence and provider-first materialization are authority-server handle methods; adapters keep redirect/query/HTML shaping.
live-account-provider-runtime-owned MUST Provider-backed runtime reads use the authority-server LiveAccountRuntimeProvider seam and supervisor, not route-owned gateway lookup.
message-operations-provider-owned MUST Message/query runtime methods own provider gateway lookup, query evaluation, cache triggers, and event publication; query_mail_page serves mail-list/search and adapters add no route-shaped list methods.
event-subscription-runtime-backed MUST Event backlog replay and live broadcast subscription are runtime-owned; adapters map returned domain events to their transport wire shape.
contract-dependencies-transport-free MUST The contract crates (posthaste-contract-core, posthaste-runtime-api, posthaste-client-link) stay limited to transport-free support crates and posthaste-domain-model/posthaste-replica-core, free of Axum, Tauri, provider-client, SQLite, store-row, and frontend packages; posthaste-contract-core stays wasm-pure.
loopback-local-auth MUST Loopback bridge access is local-only, capability-protected, host-validated, origin/referer-checked, header-token-only (query-token rejected), and writes no daemon port/token file in bundled mode.
tauri-command-allowlist MUST The desktop host exposes only named required Tauri commands with validated untrusted input and no generic filesystem/shell/process/network/environment capability.
external-navigation-restricted MUST Renderer-provided external URLs open only for allowed schemes such as http/https; untrusted in-app navigation is blocked.
no-runtime-root-deletion-from-renderer MUST Renderer storage cleanup requests runtime cleanup through named mutations/maintenance rather than deleting runtime-owned roots directly.