API implementation reference and patterns¶
This is the debug/extend depth for the /v1 API: the standards the contract is
built on, what each endpoint group means and how its handlers behave, search
scope semantics, the non-renderer client consumers, and the artifact/test
discipline. The route inventory itself is the generated
endpoint inventory.
1. Reference models¶
OpenAPI is the source format for the HTTP contract and generated HTTP clients. AsyncAPI is the source format for the event-stream contract, with Server-Sent Events the transport reference for /v1/events.
CloudEvents is a useful event-envelope reference, but Posthaste does not adopt it as the event shape unless third-party event interoperability becomes a requirement: ordering, sequence resume, and before/after state assertions matter more than generic envelope compatibility.
JMAP's push and state-change model is a protocol reference for opaque state and recovery, but Posthaste exposes its own /v1 API rather than proxying JMAP methods.
2. Endpoint semantics¶
Per-group behavior over the routes listed in the endpoint inventory. The generic handler shape (extract → validate → delegate → project → map errors → publish) is in L2 §3.
2.1 Health and artifacts¶
Health and artifact routes do not use domain service state beyond what is needed to build or serve artifacts. HealthResponse is { status: "ok" }. They are used by clients, tests, generated client tooling, and humans inspecting the API.
2.2 Settings and automation¶
AppSettings includes authority-server-owned application settings: automation rules, automation draft state, and cache policy. Automation rules have id, name, enabled, triggers, condition, actions, and backfill; conditions use the smart-mailbox rule tree. A settings PATCH replaces the full active automationRules list when present and preserves it when omitted; the same replacement rule applies to automationDrafts. Settings mutations publish ordered coarse events after persistence.
POST /automation-rules:preview evaluates a draft rule condition against local indexed projections and returns newest-first matching previews (default limit 5, max 50); it does not mutate configuration. Global appearance preferences are client-owned presentation state and are not part of AppSettings.
The automation rules CRUD surface is separate from the settings PATCH path and from the preview route. GET /rules (Read) returns the merged ruleset as RulesListResponse { rules }; POST /rules (Manage) creates a GUI-managed rule from WritableRuleInput and returns the created Rule; PUT /rules/{rule_id} (Manage) replaces it (note: replace via PUT, not PATCH); DELETE /rules/{rule_id} (Manage) returns OkResponse { ok: true }. Rules are config-file-backed (rules.toml), and a GUI-created rule cannot carry an exec action — WritableRuleAction makes exec unrepresentable, so only the file-authored ruleset can run code (see scripting security). These routes are mounted by the posthaste-server composition root (build_rules_router), not the posthaste-http-api-adapter router, and merged into /v1 at startup; a reader auditing only the adapter's router.rs would miss them.
2.3 Accounts, OAuth, and assets¶
Account handlers combine config writes, secret writes, runtime lifecycle changes, OAuth flow state, projection construction, and event publication. POST /accounts creates account config, applies the secret instruction, validates required provider fields, starts/updates runtime state, and returns AccountOverview. PATCH merges provided fields (omitted preserved); secret handling uses keep, replace, and clear modes.
DELETE removes the configuration and its managed OS secret, stops its runtime, and emits a deletion event. Enable/disable toggle the enabled flag and converge the runtime set. Verify attempts provider connection setup and reports success, primary identity email, and push support. The create/patch/delete/enable/disable paths converge authority-server runtime state after the config write.
POST /oauth/start starts provider-first OAuth; POST /accounts/{id}/oauth/start starts it for an existing account. The start response carries the authorization URL, state, and redirect URI; the callback validates the one-time state, exchanges the code, validates identity material, stores the token set as a secret, and creates/updates the account. Account overviews include resolved appearance data; logo upload accepts PNG/JPEG/WebP/GIF up to 2 MiB and logo reads return bytes by opaque image ID.
2.4 Capability token minting¶
The token mint route reads the presented token extension when auth is enabled, appending requested caveats to the caller token, or mints from the root key when auth is disabled and no caller token exists. The handler validates empty action lists and invalid expiry values before minting, and never widens authority beyond the caller token.
2.5 Typed read calls¶
POST /read executes a bounded list of named read calls in order (at most 16). Supported operations: Account/list, Mailbox/list, SmartMailbox/list, and Tag/list. Later calls may reference earlier Account/list results via "#<callId>.ids" / "#<callId>.enabledIds"; the handler resolves references before executing the dependent call. Invalid call IDs, invalid result references, unsupported operations, or authority-server read failures fail the whole request with one API error.
2.6 Smart mailboxes¶
Smart mailbox definition handlers read and mutate config-backed definitions. Message/conversation list handlers evaluate the saved rule against local indexed projections, with optional query text and pagination, and return authority-server-evaluated query pages. Definition changes emit smart-mailbox before/after assertions; clients derive affected scopes from those state changes.
2.7 Conversations and messages¶
Conversation and message list handlers validate pagination, sort, and query parameters, then call local projection reads and convert to API projection states. Message reads include ConversationRef. Conversation handlers return derived envelopes or views built from the complete authority-server message set for the requested conversation. Message detail handlers return sanitized body data and may trigger lazy body fetch through authority-server services when the local cache is missing. Attachment handlers return authenticated bytes for a message attachment; filename and presentation behavior belong to clients.
2.8 Compose and commands¶
Compose read handlers return sender suggestions (GET /sender-addresses), account identity, and reply context. The write commands — POST .../commands/send, save-draft, delete-draft, and the message-mutation commands (set-keywords, add-to-mailbox, remove-from-mailbox, replace-mailboxes, destroy) — accept an optional Idempotency-Key header: a retried request with the same key is deduplicated, and reusing a key for a different operation returns 409. SendMessageRequest may include from: Recipient; the route source_id remains the submitting account. The API rejects unnamed attachments, invalid base64, more than 10 attachments, any decoded attachment over 10 MiB, or total decoded attachments over 25 MiB with invalid_compose. The send route has a larger body limit than ordinary JSON routes because attachments are base64 in the body. After a successful send the authority server records the selected sender in the sender cache; failed/rejected sends do not. Message detail HTML is sanitized in Rust before response serialization. Command handlers delegate through the runtime contract — provider access is owned by the authority server's account supervision — and must cause resulting state-change assertions to be appended and published after the change is durable.
2.9 Sync and reload¶
Manual sync requests enqueue or run account-scoped sync through the runtime contract and return the trigger result. TriggerSyncRequest.mode defaults to incremental; fullMetadata performs an authoritative metadata refresh while preserving body/blob cache entries where possible. Config reload reads configuration from disk, computes changes, updates runtime state, and publishes config/resource events. Sync writes produce the same message change assertion shapes as command writes.
2.10 Runtime links and views¶
Runtime link streams use SSE id values equal to RuntimeFrame.linkSeq. Account-scoped clients pass sourceId on link/open-view/stream/mutation requests; the runtime rejects views and mutation operations (typed MailOperation payloads) outside the authorized account scope. Link-scoped mutations require both the mutation action and link-read authority for the same source, because settlements are delivered on the link stream. The sessionless per-view bridge (POST /views + GET /views/{view_id}/stream) is deleted (D51/M10): renderer state delivery is exclusively the link-scoped RuntimeFrame stream now — the migration this bridge existed for is complete.
3. Search query semantics¶
Query text is relative to the endpoint alias. GET /sources/{source_id}/messages?mailboxId=inbox&q=from:Alex is equivalent to in:{source_id}/inbox from:Alex; omitting mailboxId is equivalent to in:{source_id}/ from:Alex; a smart mailbox route is equivalent to in:<smart-mailbox-id> ...; opening a derived conversation uses conversation:<conversationId>. in:<name> without a source qualifier resolves to a saved smart mailbox, while in:<source>/ and in:<source>/<mailbox> resolve to concrete source scopes. Global search uses /messages/search explicitly.
The authority server parses and evaluates query text against local indexed projections; for example from:Alex matches sender name or email by authority-server query semantics. API or renderer code may validate syntax and offer completions, but result membership is authority-server-owned. Invalid query text returns invalid_query.
4. Client consumers¶
Bundled application renderers may use an in-process or IPC runtime adapter instead of /v1 for primary mail state; the adapter may use generated API types internally, but renderer semantics come from the runtime contract. Remote renderers, hosted clients, or future adapters use /v1 directly when no local runtime adapter is available. The role binaries these consumers attach to are posthaste-authority-runtime-server (the bundled all-in-one), posthaste-authority-server (the standalone far node), posthaste-runtime (the near-node daemon), and posthaste-client (the desktop app).
The MCP adapter resolves a daemon connection, uses generated API types, and maps tools to /v1 operations; it may stay stateless and use HTTP reads even when renderers use a runtime adapter. Tool failures return MCP tool errors rather than crashing the stdio transport.
Scripts and custom clients use the same /v1 auth, error, pagination, event, and state contracts. Stateless clients may ignore the event stream and use HTTP reads; long-running API clients should use SSE assertions to avoid drift.
5. Contract-artifact change checklist¶
An API implementation change that affects external shape requires:
- updating handler annotations and schema types
- updating shared projection constructors
- regenerating
openapi.jsonwhen HTTP shape changes - updating
asyncapi.jsonwhen event shape changes - regenerating web/MCP TypeScript types when OpenAPI changes
- regenerating event types when AsyncAPI changes — the web client's
EventTopicunion inapps/web/src/api/events.gen.tsis generated fromasyncapi.jsonbyapps/web/scripts/gen-event-topics.ts(bun run events:generate) and drift-checked byevents:check - updating route authorization map entries for new non-exempt operations
The generated endpoint inventory refreshes from openapi.json at doc build (just docs gen-endpoints), so step 3 keeps it current automatically.
6. Validation and tests¶
API boundary tests exercise real routers, stores, config repositories, auth middleware, and handlers, preferring behavior-level assertions over direct handler calls.
Contract tests fail when generated OpenAPI differs from committed openapi.json, when committed event topics drift from the authority-server domain topic list, or when a non-exempt OpenAPI operation has no authorization map entry (or the map has stale route entries).
Event convergence tests replay emitted assertions into a minimal client replica and compare the result with authority-server read projections; a mutation or sync path that emits incomplete assertions fails. This is the coherent-link down-channel convergence check (replication L1 §5): replaying authoritative assertions onto a near node must reproduce the far node's projection. For remote-authority deployments, settlement and the per-account/object state tokens emitted on snapshot and sync.completed together form the confirmation watermark a replica uses to retire optimistic mutations.
7. Assertions¶
| ID | Sev. | Assertion |
|---|---|---|
| html-sanitized | MUST | Message body HTML is sanitized in Rust before it reaches the API response. |
| invalid-compose-bounds | MUST | Compose rejects unnamed attachments, invalid base64, >10 attachments, any decoded attachment >10 MiB, or total decoded >25 MiB with invalid_compose. |
| read-batch-bounded | MUST | A typed read request executes at most 16 calls in order and fails the whole batch on any invalid call or authority-server read failure. |
| event-convergence-check | SHOULD | Event assertions are tested by replaying them into a client replica and comparing against authority-server projections (the coherent-link down-channel convergence check). |
| generated-clients-update | SHOULD | API adapter, MCP, remote-client, and event generated types are regenerated when their contract artifacts change. |
| endpoints-generated | SHOULD | The endpoint inventory is generated from openapi.json, not hand-maintained. |