Skip to content

Structure and flows (L2)

1. Router boundary

1.1 /v1 router

The server builds a nested /v1 router containing product API routes, contract artifact routes, fallback API 404 behavior, auth middleware, and shared application state.

The /v1 router is shared by production runtime and integration tests so tests exercise the same handlers and auth perimeter as the running server.

1.2 Outer server layers

The full server may wrap the /v1 router with CORS, tracing, static file serving, Swagger UI, or desktop/browser app serving. Those outer layers do not change API endpoint semantics. Layer order is:

CORS
 -> tracing
 -> auth middleware on /v1 router
 -> routes

1.3 Fallback behavior

Unknown /v1 paths return structured API errors. Static web fallback applies outside the API namespace.

2. Auth perimeter

2.1 Request checks

When auth is enabled, the auth middleware checks requests in this order:

  1. validate the Host header against the allowlist
  2. exempt liveness and contract artifact routes from bearer-token auth
  3. validate browser Origin or Referer against the allowlist when present
  4. verify the bearer token from the Authorization header
  5. enforce route authorization and token caveats

Host validation runs before route exemptions.

2.2 Exempt routes

The token-auth exempt routes are GET /v1/health, GET /v1/openapi.json, and GET /v1/asyncapi.json. The Swagger UI mounted at /v1/docs is outside the nested API router and uses the served OpenAPI artifact.

2.3 Route authorization map

Every non-exempt OpenAPI operation has an authorization entry keyed by method and route template. The entry defines the required action, the resource axes extracted from path or query values, and a gate or filter mode. Missing authorization entries fail closed. A completeness test compares OpenAPI operations against the authz map.

2.4 Gate and filter modes

Gate routes have enough request information to decide whether the token can access the addressed resource.

Filter routes are aggregate reads. They satisfy resource caveats only when the required query filter is present and matches the caveat. If an aggregate route cannot safely enforce a resource caveat, scoped tokens are denied.

3. Handler flow

3.1 Standard flow

A handler follows this shape:

extract state, path, query, body, and extensions
 -> validate API-level input
 -> delegate to the runtime contract (the handler's only I/O path)
 -> map runtime data to API mail-state projections
 -> map runtime/API error to ApiErrorBody
 -> publish or return events when the operation creates observable state changes

Handlers hold no service, store, or secret contracts of their own: every read and mutation goes through the runtime handle. The API layer is a protocol adapter over the runtime contract, not a second orchestration layer.

3.2 Local read handlers

Local read handlers use runtime reads that do not require live provider access — settings reads, account list/get, mailbox list, smart mailbox definitions, source message lists, conversation lists, and typed read calls.

3.3 Provider-backed handlers

Handlers that need remote provider access delegate through the same runtime contract; provider connections are owned by the authority server's account supervision, never created by handlers. These include account verification, send, message commands, manual sync, lazy body/attachment fetch when content is not cached, and identity/reply-context reads when provider data is required.

3.4 Error mapping

Handlers return ApiError. API validation errors are raised directly by handlers. Domain errors are mapped from service error kinds into stable API error codes and HTTP statuses. Error bodies are serialized as camelCase JSON with snake_case error codes.

4. Projection construction

4.1 Shared constructors

HTTP reads, SSE live events, collapsed catch-up, and snapshot mode use shared constructors for API mail-state projections. A message summary returned in an HTTP page and the before/after values in a message assertion must not be hand-shaped in separate code paths.

When a projection has multiple variants, each variant has a name and a documented purpose. For example, MessageSummaryState excludes body content while MessageDetailState includes sanitized body alternatives.

4.2 Message states and conversation references

Message assertions use MessageChangeAssertion; its before and after values are MessageSummaryState | null. The message constructor always includes ConversationRef { conversationId, conversationToken }, bodyToken, and attachmentToken when a summary is present.

The conversation, body, and attachment tokens are produced by mail-state projection code. API code transports the tokens; it does not define token internals.

4.3 Conversation envelopes and views

Conversation endpoints use derived projection constructors, returning ConversationEnvelope or ConversationView for one conversation ID and token.

The event stream does not emit conversation projection assertions for message changes. If a client has a stale visible conversation, the changed ConversationRef in the message assertion is enough to trigger an HTTP envelope/view fetch.

4.4 Query pages and invalidations

QueryPage represents a finite response for one canonical query/list scope. Initial scopes include source mailbox lists, smart-mailbox lists, global search, and conversation lists.

A query page is an exact authority-server result for one QueryScope. Query membership comes from HTTP query/list reads, not client-side filtering. Events carry state assertions; API clients that keep local views recover exact query state through HTTP reads.

5. Event stream flow

5.1 Live stream

GET /v1/events validates query filters and auth scope, replays backlog when afterSeq is present, then subscribes to live broadcasts. Each emitted domain-event item sets id to the authority-server sequence number and data to the serialized DomainEvent.

5.2 Gap detection

The live stream reports server-side broadcast lag as an explicit gap frame. Clients respond by using collapsed catch-up from their last applied sequence or by taking a snapshot when catch-up is not sufficient.

5.3 Collapsed catch-up

Collapsed catch-up is finite. The handler reads event history after afterSeq, collapses message assertions to the first retained before and latest after per (account, messageId), keeps other state assertions and coarse events according to their topic rules, and returns the resulting ordered SSE frames. It uses the same DomainEvent and message assertion payloads as live streaming.

5.4 Snapshot flow

Snapshot mode is finite and scope-bound:

validate snapshot query
 -> resolve account/object scope
 -> read current authority-server projection
 -> compute watermark sequence and state token
 -> emit snapshot.begin
 -> emit snapshot.assertion frames using normal projection payloads
 -> emit snapshot.end

Snapshot assertion payloads are built with the same projection constructors used by HTTP reads and live events.

5.5 Resume behavior

Clients store the last applied authority-server event sequence. On reconnect, clients pass afterSeq to receive events with greater sequence numbers. Resume is based on authority-server event history, not on provider push state.

6. Contract artifacts

6.1 OpenAPI

Rust handler annotations and schema types generate the OpenAPI document. The committed root openapi.json file is checked against the generated document. The server serves the generated OpenAPI document at /v1/openapi.json; the web and MCP TypeScript clients generate types from the committed artifact. The generated endpoint inventory is derived from this same openapi.json.

6.2 AsyncAPI

The AsyncAPI document describes the event stream contract: topics, SSE modes, message change assertion envelopes, other state assertion envelopes, snapshot control frames, and coarse payloads. The server serves it at /v1/asyncapi.json. A contract test checks the event topic enum in AsyncAPI against the authority-server domain event topic list.

6.3 Shared projection drift checks

OpenAPI and AsyncAPI must not define divergent shapes for the same named projection. A drift check should compare the named schemas used by HTTP and event assertions once the generator can expose them from one source. Until one generator owns both artifacts, projection constructors and convergence tests are the enforcement boundary. The full change procedure for a shape change is the checklist in L3 §5.

7. Assertions

ID Sev. Assertion
shared-router-tests MUST Integration tests use the same /v1 router and auth perimeter as production runtime.
host-before-exempt MUST Host validation runs before token-auth route exemptions when auth is enabled.
authz-map-complete MUST Every non-exempt OpenAPI operation has a route authorization entry.
unmapped-fails-closed MUST A scoped request to an unmapped protected route fails closed rather than allowing access.
handlers-delegate-domain SHOULD Handlers validate transport input and delegate domain decisions to authority-server contracts.
shared-projection-constructors MUST HTTP reads, live events, collapsed catch-up, and snapshots use shared constructors for the same named projections.
message-conversation-ref MUST Message HTTP reads and message assertions include ConversationRef.
message-before-after MUST Message assertions carry before and after MessageSummaryState values with nulls for create/destroy.
message-content-tokens MUST Message HTTP reads and message assertions include body and attachment tokens.
no-message-conversation-duplication MUST Message changes do not emit separate conversation projection assertions.
query-pages-authority-server-evaluated MUST Query pages use authority-server query evaluation instead of client-side filtering.
no-query-invalidation-events MUST Event streams carry state assertions rather than query-scope invalidation commands.
backlog-before-live MUST SSE streams replay matching backlog events before live events when afterSeq is provided.
catch-up-collapses-messages MUST Collapsed catch-up returns at most one assertion per (account, messageId) with first retained before and latest after.
snapshot-uses-live-shapes MUST Snapshot assertions use the same message payload shapes as live and catch-up assertions.
generated-openapi-source MUST Served OpenAPI is generated from authority-server handler/schema source.
asyncapi-topic-check MUST AsyncAPI event topics are checked against authority-server domain event topics.
watermark-on-settlement MUST Settlement carries the originating clientMutationId and any assigned entity id, and snapshot/sync.completed carry per-scope state tokens, so a remote replica can retire confirmed optimistic mutations.