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:
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:
- validate the
Hostheader against the allowlist - exempt liveness and contract artifact routes from bearer-token auth
- validate browser
OriginorRefereragainst the allowlist when present - verify the bearer token from the
Authorizationheader - 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. |