Architecture and trust boundaries

Skrzynka Desktop is a native SwiftUI client of the local Skrzynka /v1 API. The app owns presentation, user intent, selection, and ephemeral compose text. The core service owns every durable resource and provider effect. Presentation is enti

One client, one authority#

Skrzynka Desktop is a native SwiftUI client of the local Skrzynka /v1 API. The app owns presentation, user intent, selection, and ephemeral compose text. The core service owns every durable resource and provider effect. Presentation is entirely WisentDesignSystem; the interface contract is the interface design contract.

mermaid
flowchart LR
    User[Operator] --> Auth[Wisent shared authentication]
    Auth -->|verified identity and organization| Desktop[Skrzynka Desktop]
    Desktop -->|bearer + organization over loopback JSON| Core[Skrzynka]
    Desktop -->|open trusted authorization URL| Google[Google consent]
    Google -->|one-use code to loopback callback| Core
    Core -->|verify bearer and membership| Auth
    Core -->|exact reads and Gmail authorization write| Skarbiec[Skarbiec]
    Core -->|TLS receive, password or XOAUTH2| IMAP[IMAP provider]
    Core -->|TLS reply and originated mail, password or XOAUTH2| SMTP[SMTP provider]
    Core --> SQLite[(Organization-scoped local SQLite)]

The desktop app never reads the database, starts a shell command, resolves a Skarbiec item, receives a mailbox password, OAuth code, provider access token, refresh token, or OAuth client secret, opens a mail protocol connection, or records a provider result independently. It receives a Wisent session from the shared authentication package and sends that bearer only to the loopback core; the core verifies the bearer and selected organization centrally before accessing organization-scoped local resources. During Gmail connection the app additionally holds only the selected non-secret identity, PKCE flow identifier, trusted authorization URL, and normalized flow status returned by the core.

Local state#

UserDefaults contains only:

  • the loopback Skrzynka endpoint,
  • one installation identifier for the first-run walkthrough, and the walkthrough's own progress under its versioned namespace.

Shared Wisent authentication persists its own session in Keychain. In-memory state contains that session, selected organization, mailbox, message, reply and outbound API resources, Skarbiec metadata, Google account addresses, selected identifiers, an ephemeral OAuth flow identifier and authorization URL, reply compose text, the outbound draft — sending mailbox, recipients, cc, subject, body and the idempotency key minted for it — and one failure value per surface. An organization change clears every prior organization-scoped resource and selection, including the draft, before the new bearer context becomes renderable. Mailbox passwords, OAuth codes, provider tokens, raw MIME, and OAuth client secrets are absent by contract. Flow identifiers and compose text are not restored after app termination.

Network policy#

SkrzynkaClient accepts only http URLs whose host is 127.0.0.1, localhost, or ::1, with no embedded username or password. Every request carries the current Wisent bearer and selected organization ID. Requests use an ephemeral URL session invalidated on exit, a bounded 30-second timeout, an 8 MiB response ceiling, JSON key conversion, and the core's own error envelope decoded into a failure value carrying code, the verbatim message and retryable. The core separately refuses a non-loopback bind, verifies the session and membership, and scopes database reads and writes by organization.

User-visible state#

The window is a sidebar of nine destinations in four groups — Work: Posture, Mailboxes, Connect; Mail: Inbox, Compose, Sync; Evidence: Replies, Outbound; System: Service — each a WisentScreen with a pinned 44 pt context bar. Data screens are three zones: facet rail, table or message form, inspector. The interface design contract is authoritative for the information architecture and states which rules the implementation may not break.

The model refreshes resources after mutations and preserves selections only while their authoritative resource remains present. A read that fails sets that surface's own failure value and never clears the value an earlier read produced. Every mutation reports through WisentMutationBar. Both provider-facing sends — a reply to an inbound message, and a message a mailbox originates — pass through a decision dialog before any SMTP traffic, and each carries one idempotency key: the compose surface shows the key it minted beside the send verb, and a key the core has already answered is never sent twice, so a refused send offers a new message rather than a retry.

Failure behavior#

Core unavailability leaves the window navigable, with the saved loopback endpoint and a Check service verb on the Service screen. One mailbox error is shown against that mailbox and does not hide other mailboxes. A failed reply keeps its stored code and sentence under Replies; an uncertain reply gets its own facet, a standing warning quoting the product's rule, and is never automatically sent again by the app. An originated message behaves the same way under Outbound, where failed and uncertain are also the sidebar count and a Posture panel, and the only verb offered composes a new message under a new key.