The problem
I got tired of email clients. Not because email is bad — JMAP fixed the protocol problem. But every client looks the same: three columns, a compose button, maybe a dark theme if you're lucky. They're all Outlook clones with different logos.
Meanwhile, I'm running Stalwart Mail Server on my own infrastructure, and I want a client that actually takes advantage of what JMAP can do. Delta sync. WebSocket push. Real-time state. Most clients treat JMAP like IMAP with extra steps.
The idea
Star Trek's LCARS interface isn't just cool aesthetic — it's a design philosophy. Information is organized by function and priority. Color communicates status. The interface is unmistakably *itself*.
What if an email client worked like that? What if your inbox was "Incoming Transmissions"? What if starred emails were "Priority One"? What if the whole thing felt like operating a starship's communications array?
Why Stalwart
Stalwart Mail Server is the only JMAP server I'd trust for this. It's Rust-based, fast, standards-compliant, and handles the full JMAP spec including push. It supports OAuth2/OIDC out of the box. And its authentication layer is extensible enough to support WebAuthn passkeys — which is how we get to passwordless login.
The JMAP protocol is genuinely better than IMAP. Batched requests, typed responses, WebSocket push — it's what email syncing should have always been. Stalwart implements it properly.
The architecture
Stich is a monorepo with shared packages:
- ▸
@stich/core— JMAP types, domain models, sync state - ▸
@stich/api— JMAP client, OAuth2/OIDC, WebAuthn passkeys - ▸
@stich/storage— TanStack DB collections (mailboxes, emails, threads, identities, vacation) - ▸
@stich/ui— Shared component library
And platform apps:
- ▸Desktop — Electrobun (Bun native window + WebView)
- ▸Web — TanStack Start with SSR
- ▸Mobile — React Native + Expo (in progress)
The key architectural decision was using TanStack DB as the local store. Instead of making individual API calls for each component, we load data into normalized collections and use live queries to reactively bind data to the UI. When a WebSocket push comes in with a state change, we write it directly to the collection and every active query updates in under a millisecond.
Passkeys instead of passwords
I didn't want to store passwords. Period. WebAuthn with platform authenticators means the user's device itself is the authentication factor. A passkey is created during registration, stored in the device's secure enclave, and used for subsequent logins. No password to forget, no password to phish, no password to leak in a breach.
The flow is:
1. POST /auth/passkey/register-begin → server returns a challenge 2. Browser calls navigator.credentials.create() → user authenticates with fingerprint/face 3. POST /auth/passkey/register-finish → server validates attestation, returns OAuth2 access token 4. Access token used for all subsequent JMAP calls
Authentication is similarly clean: challenge → sign → token. No password field anywhere in the app.
What's working
- ▸JMAP session fetch and full API client
- ▸Passkey registration and authentication
- ▸TanStack DB collections with live queries for mailboxes, emails, threads, identities
- ▸WebSocket push with exponential backoff reconnection
- ▸Email list, viewer, compose/reply/forward
- ▸Sandboxed HTML email rendering
- ▸Search, mark read, archive, delete, move to trash
What's next
- ▸Mobile app (React Native + Expo)
- ▸Calendar view with CalDAV sync
- ▸Cal.com booking integration
- ▸Full offline mode with sync queue
- ▸Multi-account support
- ▸Encrypted email (S/MIME + PGP)
Why the name
Stich — it's a stitch in the fabric of communication. Connecting the old world of email with the new world of local-first, passwordless, reactive applications. Also it sounds cool when you say it out loud.
Engage.