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.
2. The link is replication-only¶
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.
3. The AuthorityServerLink contract¶
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-channel —
forward_mutation(MutationRequest) -> MutationReceipt. Forward a (possibly client-originated) mutation — the typedMailOperationcarried in theMutationRequest, parsed once at the wire, never a stringlyname/argspair — toward the authority server with a stable mutation id; the receipt carries the authority server'sRuntimeMutationIdfor 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 carriesforward_mutation_for(&AuthorityServerLinkId, MutationRequest)— the runtime-aware entry the link server uses, threading the credential-derivedAuthorityServerLinkIdinto the far node's per-runtime registry; the client-side impls (the runtime itself) callforward_mutation. - Down-channel —
subscribe(LinkCoverage) -> DownStreamof orderedBaseAssertions + per-mutation confirmation.BaseAssertions are broadcast to every connected runtime (each filters by its coverage);AuthorityServerFrame::Settlementis routed only onto the down-stream of the runtime that forwarded the mutation (settlement-routed-to-origin-runtime). The runtime-aware entrysubscribe_for(&AuthorityServerLinkId, LinkCoverage)merges the broadcastBasewith the originating runtime's routedSettlements; the client-side impls callsubscribe. The near node rebases its base cache on each frame and recomputes its derived views (never invalidates). - Read channel —
query_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. |