Skip to content

Seam contract (L1)

1. Boundary

This sub-domain is the runtime↔authority-server coherent link (replication L1 §2) as a real, switchable seam in the implementation. The runtime is the near node; the authority server is the far node. The two are co-located in one process by default (the bundled application) and split across machines when configured remote — the same mechanism either way (replication L1 §2.4, transport-neutral-link). The far node may serve multiple runtime near nodes at once (replication L1 §10, link-permits-fan-in); identity, idempotency, and settlement are then scoped per runtime (§3.1).

The mechanism is not new: it reuses posthaste-replica-core (the predictor + MessageReplica convergence engine) and posthaste-replica-projector (the read-model cache) proven for the client link. This seam applies them to the second link.

The runtime reaches the authority server only through the link's channels — never by reading the authority server's store across the link (authority-server-link-is-replication-only, crates/posthaste-authority-server/src/backend.rs::Backend). Everything the runtime serves (view rows, message detail, counts, the current-state reads behind undo-history) is either derived locally from its base cache or read through the link's read channel on a miss. Only mutations (up), assertions (down), and read-through requests cross the link.

The seam is two traits mirroring the client↔runtime seam's shape (RFC D33): AuthorityServerApi — the typed request surface, what the runtime may request of the authority server (reads, account/settings operations, apply) — and AuthorityServerLink — the replication contract proper, the subject of this section. The crate carries only the authority-server-link surface (the trait, the frames, the link identity, the wire paths); the shared wire vocabulary the request payloads are built from (MailOperation, MutationRequest/MutationReceipt, the ids) lives in posthaste-contract-core and is imported, not redefined. The contract carries three channels:

  • Up-channelforward_mutation(MutationRequest) -> MutationReceipt. Forward a (possibly client-originated) mutation — the typed MailOperation carried in the MutationRequest, parsed once at the wire, never a stringly name/args pair — toward the authority server with a stable mutation id; the receipt carries the authority server's RuntimeMutationId for the confirmation join. Idempotent on the (AuthorityServerLinkId, ClientMutationId) composite: the authority server dedups a runtime's retried mutation to its existing record (per-runtime-idempotency). The trait also carries forward_mutation_for(&AuthorityServerLinkId, MutationRequest) — the runtime-aware entry the link server uses, threading the credential-derived AuthorityServerLinkId into the far node's per-runtime registry; the client-side impls (the runtime itself) call forward_mutation.
  • Down-channelsubscribe(LinkCoverage) -> DownStream of ordered BaseAssertions + per-mutation confirmation. BaseAssertions are broadcast to every connected runtime (each filters by its coverage); AuthorityServerFrame::Settlement is routed only onto the down-stream of the runtime that forwarded the mutation (settlement-routed-to-origin-runtime). The runtime-aware entry subscribe_for(&AuthorityServerLinkId, LinkCoverage) merges the broadcast Base with the originating runtime's routed Settlements; the client-side impls call subscribe. The near node rebases its base cache on each frame and recomputes its derived views (never invalidates).
  • Read channelquery_mail_page, current_summary, message_detail, conversation, list_accounts, get_account, app_settings. The query engine stays at the authority; a near node reads through here on a cache miss. Each read defaults to erroring (read_channel_unsupported), so a write-only transport stub is simply not a read source.

The runtime holds the contract through the AuthorityServerLinkHandle (Arc<dyn AuthorityServerLink>), which does not know which transport it carries.

3.1 Runtime identity and fan-in

The far node may serve multiple runtime near nodes at once (replication L1 §10, link-permits-fan-in). Each remote runtime establishes a stable AuthorityServerLinkId — its per-connection authority-server-link identity — with the authority server at link establishment, derived from its authenticated credential; the authority server tracks connected runtimes in a per-runtime registry that maps AuthorityServerLinkId to its mutation-id pairing and its settlement-routing sink. Mutation idempotency, the confirmation watermark, and settlement are scoped per AuthorityServerLinkId, never global — two runtimes may independently mint the same ClientMutationId without collision. AuthorityServerFrame::Base is broadcast to all runtimes (each filters by its coverage); AuthorityServerFrame::Settlement is routed only to the originating runtime's down-stream, mirroring the runtime adapter's per-link settlement routing (settlement-frame-scope, runtime/adapter L1 §8) one level up.

There is no single-runtime special case. An in-process link is the X = 1 case — the embedded runtime is the only near node, and it mints a real AuthorityServerLinkId (no credential, no auth); the registry has one (trivial) entry under it, and confirmation remains synchronous via the receipt (colocated-unchanged). A remote link is the X ≥ 1 case: every connecting runtime authenticates with a distinct credential that names (or derives) its AuthorityServerLinkId (runtime-credential-per-runtime), and the link server threads that id into forward_mutation_for. There is no shared-bearer mode — a single remote runtime is simply the N = 1 case with one credential.

[::state partial plan=eph/DESIGN-L2-deployment-topology#4-multi-runtime-fan-in]

4. Reads are read-through

The query engine is the authority's — filtering, sorting, and paginating a mail list runs at the authority server, which owns the whole store. What a near node caches is the data that flowed back (the rows a query returned, the messages a detail read fetched), kept coherent by the down-channel, not a query engine. Re-serving a known list and point reads are local once cached; a new query or page is a read-through to the authority.

A point read the cache cannot satisfy is a read-through on a miss — the same primitive as the co-located read, paid over the link. There is no separate point-read channel and no assertion to relax. Undo-history needs no special case: the message it reads is usually already cached, reads through on a miss, and is simply unavailable offline-and-uncached (you cannot undo what you never saw).

5. Coverage and transport neutrality

The authority server authors coverage on the base it serves down (replication L1 §3.3) so the runtime can distinguish "absent because unchanged" from "absent because not held." The co-located runtime requests complete coverage; a split runtime requests a working set.

A link's transport (in-process vs remote) is chosen from runtime configuration, not at build time, and the default is in-process co-located. With the default, the deployment behaves identically to the pre-link implementation (colocated-unchanged): the seam extraction is behavior-preserving by construction, because co-located the read policy is passthrough (no cache, always read through) and the up-channel confirms synchronously (the pending set is empty).

6. Assertions

ID Sev. Assertion
authority-server-link-is-replication-only MUST The runtime reaches the authority server only through AuthorityServerLink's typed-operation up-channel, base-assertion down-channel, and read-through read channel; it never reads the authority server's store across the link.
transport-selected-by-config MUST A link's transport (in-process vs remote) is chosen from runtime configuration, not at build time; the default is in-process co-located.
colocated-unchanged MUST With the default in-process transport, the co-located deployment behaves identically to the pre-link implementation (the seam extraction is behavior-preserving).
reads-are-read-through MUST A near node serves reads from its policy cache on a hit and reads through to the authority on a miss; co-located the policy is passthrough (no cache). The query engine stays at the authority.
authority-server-link-coverage SHOULD The authority server authors coverage on the served base so the runtime distinguishes "absent because unchanged" from "absent because not held."
runtime-identity MUST Every runtime has a real AuthorityServerLinkId: a remote runtime establishes a stable one at link establishment (derived from its credential); an in-process link is the X = 1 case, minting its id (no credential). The authority server tracks connected runtimes in a per-runtime registry.
settlement-routed-to-origin-runtime MUST AuthorityServerFrame::Settlement is delivered only on the down-stream of the runtime that forwarded the mutation; AuthorityServerFrame::Base is broadcast to every connected runtime (filtered by coverage).
per-runtime-idempotency MUST The authority server dedups forwarded mutations by the (AuthorityServerLinkId, ClientMutationId) composite; a runtime's retried mutation resolves to its existing record, never a second application.
runtime-credential-per-runtime MUST A remote authority server authenticates each runtime with a distinct credential that names (or derives) its AuthorityServerLinkId; there is no shared-bearer mode — a single remote runtime is the N = 1 case with one credential.