Skip to content

Coherent links (L1)

1. Boundary

1.1 Contract

The replication domain defines how authoritative mail state moves between layers so that every layer can act instantly, work offline, and still converge on a single authority. It defines one mechanism — the coherent link — and the rule for composing links into a chain.

A coherent link connects a near node to a more authoritative far node. It has two channels:

  • an up-channel: a pending set of optimistic mutations the near node has accepted but the far node has not yet confirmed;
  • a down-channel: ordered, authoritative state assertions from the far node, each tagged with how far the far node has confirmed the near node's mutations.

The near node's visible state is a pure function of the far node's last confirmed base and the near node's still-unconfirmed mutations. The link is transport-neutral: the same mechanism runs over in-process calls, IPC, localhost HTTP/SSE, or remote HTTP/SSE.

This domain owns the mechanism. It does not own which mail facts are canonical (that is Mail state), nor the renderer contract, persistence, or provider protocols (those are the runtime, client, authority-server, and API domains, which are link nodes).

1.2 Motivation

Posthaste must feel instant, work offline, and never show invented state. Those goals conflict whenever a layer is across a network from its authority. The naive resolutions fail in opposite directions: waiting for the authority makes the UI slow; letting each layer keep its own ad hoc optimistic copy makes layers drift, because nothing says when an optimistic guess becomes confirmed or how a correction propagates.

The escape is to make optimism a pure function over an authoritative base, never a stored truth. A layer shows f(confirmed_base, pending_mutations). When new authority arrives it does three mechanical things — replace the base, drop the mutations the authority confirms, recompute — and any misprediction self-heals because the layer never trusted its guess; it re-derives. There is no invalidation graph to maintain. This is the rule already stated for the down-channel as assert state, never command invalidation (state/mail L1 §7, no-query-invalidation-events); this domain generalizes it to include the up-channel and to compose across more than one link.

Defining one mechanism — instead of a bespoke pending set per boundary — is the point. The fragility this domain replaces came from coordinating "a mutation was sent" with "the projection now reflects it" by hand at each call site, which grows a new special case for every surface. One link contract, composed, removes that class of bug.

1.3 Reference models

Replicache is the closest reference: optimistic mutators, a per-client lastMutationID watermark, and rebase of pending mutations onto the authoritative base. This domain adopts that single-link model and composes two or more links into a chain, which Replicache does not specify.

JMAP is the reference for opaque authoritative state tokens and for recovering when an incremental change set cannot be calculated (snapshot fallback). CQRS is the reference for separating mutation commands from recomputed read projections.

Conflict-free replicated data types (CRDTs) are deliberately not adopted. Posthaste has a single authority per fact (the provider, surfaced by the authority server); it needs authority-wins convergence, not multi-writer merge. Operational transformation is likewise out of scope.

2.1 The chain

Mail state flows along a chain of nodes, most-authoritative last:

Client ──link──▶ Runtime ──link──▶ Authority server ──▶ providers

Each arrow is a coherent link. Providers are the final authority; the authority server is authoritative for everything below it; the runtime is authoritative for the client; the client is authoritative for nothing but is where the user acts. A far node may serve multiple near nodes at once (link-permits-fan-in): the watermark, idempotency, and settlement are then tracked per near node, and base assertions broadcast to all of them.

2.2 Node roles

Every non-authority node is, at the same time:

  • the near end of its up-link (it holds a pending set of pending mutations toward its authority and a cache of that authority's confirmed base), and
  • the far end of the link below it (it serves a confirmed base downward and confirms the lower node's mutations).

The runtime is the clearest case: it is the near end of the authority-server link and the far end of the client link. This dual role is what makes the chain compose (§6).

2.3 Authority gradient

Each node up the chain knows strictly more than the node below it: the client knows local intent; the runtime additionally knows automation rules, the full account set, and cross-account effects; the authority server additionally knows provider truth. A node may therefore correct the optimistic state of the node below it. Corrections only ever flow down, and only as authoritative base updates — never as imperative invalidation messages (§6.3).

2.4 Transport neutrality

A link's semantics do not depend on its transport. The client↔runtime link may be in-process (bundled desktop), IPC, or remote HTTP/SSE (hosted runtime); the runtime↔authority-server link may be in-process (embedded authority) or remote. The same mechanism and the same mutation/assertion vocabulary apply on every transport. Deployment changes where nodes run and which transport carries a link, not what the link means.

3. Replicated state and derived state

3.1 Replicated state

A link replicates canonical mail state (state/mail L1 §3): message identity and summary state, mailbox membership, keyword state, body/attachment tokens, and the named non-message states that carry before/after assertions (mailbox, smart-mailbox, account overview). This is the base a link ships down and the state a mutation's local effect transforms.

3.2 Derived state is not replicated

Conversations, query pages, sidebar counts, smart-mailbox contents, and list windows are derived (state/mail L1 §4). They are recomputed from replicated state, not shipped as their own replicated objects and not optimistically patched. A node recomputes a derived view when an assertion or a pending mutation intersects its dependencies; it never repairs a derived view by hand.

3.3 Coverage

A near node may hold only a subset of replicated state (a window, a scope). The far node authors coverage on the base it serves down so the near node can tell "absent because unchanged" from "absent because not held." Coverage is authority-authored metadata on served state, not something a near node infers. The bundled authority server reports complete coverage for its local reads; a partial replica reports its actual coverage.

4. Up-channel: optimistic mutations and the pending set

4.1 Named mutations

The near node submits named mutations with stable, near-node-minted mutation ids. A mutation names its intent and arguments; it is not a transport command or a direct patch of the far node's store. The mutation vocabulary is shared across links (the runtime forwards a client mutation toward the authority server as the same named mutation).

4.2 The pending set

Accepted mutations are appended to a durable, ordered pending set — the up-channel. The pending set is the only place a near node's unconfirmed intent lives. It survives restart: re-submitting an already-accepted mutation id resolves to the same record, never a second application (idempotency is durable near-node state).

The pending set drains toward the far node when reachable and buffers when not, so accepting a mutation never requires the far node to be online.

4.3 The local effect (predictor)

Each named mutation has a local effect: a pure, deterministic function apply(state, mutation) -> state over replicated state. The near node applies it immediately to produce optimistic state; the far node applies its authoritative equivalent. The local effect is the predictor in the rebase (§5).

The local effect is defined once and shared by every node that runs it. Where it runs follows deployment, not a reimplementation: in-process where the node is native to the authority's language, compiled to a portable form (e.g. WASM) where a node runs in a different host. Maintaining two hand-written predictors for one mutation is prohibited — divergent predictors reintroduce the drift this domain exists to remove.

4.4 Idempotent, coalescing assertions

State-assertion mutations (set keywords, replace mailboxes, destroy) carry a desired state, not a delta of record. Folding the same assertion twice is a no-op. The pending set may coalesce successive pending assertions for the same entity into the latest desired state; a coalesced pair that cancels out (assert then un-assert) drops to nothing. Entity-creating mutations (draft create, send) are not idempotent states and converge by identity instead (§5.4).

5. Down-channel: authoritative assertions and convergence

5.1 Authoritative assertions

The far node emits ordered before/after state assertions (state/mail L1 §7, api L1 §2.4). The after value is authoritative replicated state. Assertions are post-state, never operational deltas, and are idempotent to apply. The down-channel is resumable from a sequence cursor and recoverable by snapshot when an incremental range is lost.

5.2 The confirmation watermark

Every authoritative base the far node serves down carries a confirmation watermark: the high-water mark of the near node's mutation ids the far node has applied (a lastConfirmedMutationId, per the Replicache model). The watermark is the join between the up-channel and the down-channel — without it a near node cannot know which of its pending mutations are now reflected in the base. When a far node serves multiple near nodes (link-permits-fan-in) it tracks a separate watermark per near node; the realization is per-mutation settlement keyed by the near node's identity (e.g. the runtime↔authority-server link's AuthorityServerLinkId, authority-server-link L1 §3.1), so two near nodes minting the same mutation id never collide and a settlement routes only to its originator.

5.3 The convergence rule (rebase)

When a near node receives an authoritative base update it performs exactly one operation:

replace the confirmed base with the new authoritative state; drop every pending mutation at or below the confirmation watermark; recompute the view as replay(confirmed_base, remaining_pending).

This single operation is the whole convergence story. It subsumes per-call-site flush ordering, post-action reconciliation, and "retire on send." A mutation is retired only when the watermark confirms it — never when it was merely sent or accepted by the far node's transport. Because the local effect is idempotent (§4.4), the interval where the base already reflects a mutation and the mutation is still folded is a visual no-op, so the exact retire instant cannot flicker. A mutation that the authority rejects or changes self-heals: the recompute uses the authoritative after, not the near node's guess.

5.4 Entity convergence

Entity-creating mutations converge by identity, not idempotent fold. The near node holds a provisional entity under a near-minted id; the far node assigns the authoritative id and reports the assignment in the assertion/settlement; the near node reconciles its provisional id to the authoritative id and retires the provisional entity once a base row with that id exists. The read path dedupes by authoritative id so a provisional entity and its confirmed row never both show.

5.5 Settlement

A mutation's terminal outcome is reported as settlement: confirmed (the watermark passed it) or failed (the authority rejected it). Settlement carries the mutation id and, for entity creation, the assigned id. Failure surfaces to the user and removes the pending mutation, so the view reverts to authoritative state. There is no resting "conflict" state on the link; a divergence resolves inside convergence or fails terminally.

6. Composition

6.1 Chaining

Links compose because a node is both a near end and a far end (§2.2). The base a node serves down is its own recomputed state — replay(its confirmed base from above, its pending mutations toward above). The watermark it serves down is over the mutation ids it received from below and has itself accepted.

6.2 Forwarding

A node forwards a mutation it accepted from below toward its own authority as the same named mutation, preserving the originating mutation id. The originating id travels the whole chain so the final authority's confirmation can be matched back at the node that minted it. A node confirms a lower mutation to that lower node according to its own confirmation policy (§7).

6.3 Corrections cascade as base updates

A node never sends an invalidation across a link. When the authority server corrects the runtime, the runtime recomputes and serves a new authoritative base down to the client; the client rebases on it (§5.3). The client needs no knowledge of the authority server, and no special handling for "the thing the runtime told me was superseded": it adopts its immediate authority's new base. End-to-end coherence is the composition of independent single-watermark rebases. This is the load-bearing property of the chain — it is why there is no cross-layer invalidation logic to get wrong.

7. Confirmation policy and provenance

7.1 Confirmation levels

A node may confirm a lower node's mutation when it accepts it optimistically (fast, the node predicted the effect) or only when its own authority confirms it (slow, ground truth has passed). Both are valid; the difference is provenance, not correctness, because a later correction always arrives as a base update (§6.3). A node that confirms on optimistic acceptance still re-serves a corrected base if its authority later changes the result.

7.2 Provenance is authority-authored

How confirmed a value is — local guess, accepted by the runtime, settled by the provider — is exposed as authority-authored markers on served state and as settlement, not as something a near node infers from its own pending log. "Pending until it reaches the provider" is a marker the runtime authors on the row; the renderer renders it. Provenance is part of coverage (§3.3).

8. Offline and recovery

8.1 Both directions buffer

A link tolerates either end being unreachable. The up-channel (pending set) buffers accepted mutations until the far node is reachable. The down-channel buffers authoritative assertions, durably and ordered, until the near node reconnects and resumes from its cursor. Together they are the data bus that lets any node operate while a neighbor is down.

8.2 Resume and snapshot

A reconnecting near node resumes the down-channel from its last applied sequence. If the far node cannot calculate the missed range (gap), the near node recovers by snapshot: an authoritative current-state pull for the scope, applied with the same assertion primitive, after which it resumes incremental delivery from the snapshot watermark. Derived views are recomputed, never resumed.

8.3 Order before observation

A node observes a new authoritative state only after the far node's durable write that produced it (the state-before-event rule, authority-server L1 §8.2). A near node that applies an assertion can read a state consistent with that assertion or newer.

9. Failure and backstops

9.1 Permanent rejection

A mutation the authority can never apply (validation, unsupported, irreversibly rejected) fails terminally, settles as failed, surfaces to the user, and leaves the pending log. Send is never silently retried after an interrupted in-flight attempt, because submission is not idempotent; it fails for the user to decide.

9.2 Never-confirms backstop

A mutation accepted by the authority but whose confirmation never arrives (provider eventual-consistency, a scope never re-observed) must not linger unbounded. After a bounded number of convergence cycles a near node retires such a mutation and records the event for telemetry. The retire is safe because the authoritative base is what shows; the backstop exists to bound the pending log and to surface a stuck authority, not to change visible state.

9.3 Idempotent delivery

Both channels assume at-least-once delivery and idempotent application: mutation ids dedupe the up-channel, post-state assertions dedupe the down-channel.

10. Deployment topology

The same mechanism appears at every boundary; deployment chooses placement and transport, not link meaning. Three component scopes — authority server (store + providers + far node), runtime (near node: links, views, pending set, read-through cache), client (renderer) — compose over the two links (client↔runtime, runtime↔authority-server). A scope's far side is local (in-process, co-located) or remote (over the link); the links are the only coupling, nothing else crosses a scope boundary.

The named deployment instances are presets over this free composition (free-combination):

  • Bundled application (the default). Authority server + runtime + client co-located in one process; both links in-process. The renderer holds no pending set because its authority is local and instant. Identical to the pre-topology desktop app (colocated-default-unchanged).
  • Hosted runtime / multi-device. The runtime is remote; the client↔runtime link runs over a remote transport, so the client layer is a full near node (a replica) running the shared local effect.
  • Hosted authority server. The authority server is remote from the runtime over the runtime↔authority-server link, with the runtime a buffering near node.
  • Hosted authority server, multi-runtime. Multiple runtime near nodes connect to one remote authority server over authenticated runtime↔authority-server links, each with its own AuthorityServerLinkId (authority-server-link L1 §3.1). AuthorityServerFrame::Base is broadcast to all runtimes; AuthorityServerFrame::Settlement is routed per runtime. The single-runtime hosted-authority-server preset is the N=1 case of this one.
  • Daemon. Headless authority server + runtime in-process, serving clients.

The deployment rules:

  • Authenticated remote links (remote-links-authenticated). Every link surface reachable off the local process is behind authentication; an in-process link needs none. No unauthenticated remote link ships.
  • HTTP is the universal transport (http-universal-transport). HTTP(+SSE) carries every remote link; Tauri-native IPC is an optional bundled-case optimization that never gates the topology.
  • The control app is the only switcher (control-is-the-only-switcher). Topology switching (which scopes run, which links are local vs remote) lives in the control app's topology config; the lean role binaries are fixed-role. A change applies by restart (switch-by-restart), not hot-swap.

The build seam, lean role binaries, config-selected transports, and authenticated links are realized (authority-server-link L2 §6-§7). Productizing the topology — the control-pane UI, the client-link transport selector, and the optional Tauri-native IPC node — is in flight.

[::state partial plan=eph/DESIGN-L2-deployment-topology]

In every case the renderer owns no authority and the named mutation / assertion / watermark vocabulary is unchanged.

11. Assertions

ID Sev. Assertion
link-two-channels MUST A coherent link has an up-channel pending set of pending mutations and a down-channel of authoritative before/after assertions.
view-is-pure-fold MUST A node's visible replicated state is replay(confirmed_base, pending_mutations); optimism is never stored as truth.
recompute-not-invalidate MUST Authority changes are applied by replacing the base and recomputing; nodes never send cross-layer invalidation commands.
single-local-effect MUST A named mutation's local effect is defined once and shared by every node that runs it; no hand-written duplicate predictor per language.
confirmation-watermark MUST Every authoritative base served downward carries a watermark of the near node's confirmed mutation ids.
retire-on-confirmation MUST A pending mutation is retired only when the confirmation watermark passes it, not when it was sent or transport-acknowledged.
idempotent-assertion-fold MUST State-assertion mutations carry desired state and fold idempotently, so the base-updated-and-still-pending interval is visually a no-op.
entity-convergence-by-id MUST Entity-creating mutations reconcile a provisional id to the authoritative id and dedupe by authoritative id; a provisional entity and its confirmed row never both show.
mutation-id-end-to-end MUST An originating mutation id is preserved as the mutation is forwarded along the chain so the final confirmation matches back at the minting node.
corrections-as-base-updates MUST A correction at a higher authority propagates downward as a new authoritative base, not as an invalidation message; lower nodes rebase.
pending-set-durable-idempotent MUST The pending set is durable and idempotent on mutation id across restart; an accepted mutation is applied at most once.
provenance-authority-authored MUST Confirmation/provenance is exposed as authority-authored markers and settlement, not inferred by a near node from its own pending log.
transport-neutral-link MUST Link semantics and the mutation/assertion/watermark vocabulary are independent of in-process, IPC, localhost, or remote transport.
derived-not-replicated MUST Conversations, query pages, and counts are recomputed from replicated state, never optimistically patched or shipped as replicated objects.
offline-both-directions MUST A link buffers accepted mutations up and authoritative assertions down so either end can be unreachable without loss.
snapshot-recovery MUST A near node that loses an incremental range recovers by authoritative snapshot, then resumes incremental delivery from the snapshot watermark.
permanent-failure-surfaces MUST A terminally rejected mutation settles as failed, surfaces to the user, and reverts the view to authoritative state.
never-confirms-backstop SHOULD A mutation accepted but never confirmed is retired after a bounded number of convergence cycles and recorded for telemetry.
remote-links-authenticated MUST Every link surface reachable off the local process is behind authentication; in-process links need none; no unauthenticated remote link ships.
http-universal-transport MUST HTTP(+SSE) is the universal link transport; IPC is an optional bundled-case optimization that never gates the topology.
control-is-the-only-switcher MUST Topology switching lives in the control app's topology config; lean role binaries are fixed-role; a change applies by restart, not hot-swap.
colocated-default-unchanged MUST The default bundled preset behaves identically to the pre-topology desktop app.
link-permits-fan-in MUST A far node may serve multiple near nodes over the same link; the confirmation watermark, mutation-id idempotency, and settlement are tracked per near node, and base assertions broadcast to all near nodes.