Synesis

Why my mail lives in SQLite before it lives anywhere else

Every email client eventually asks you to stare at a circle.

Sometimes it’s polite: a little spinner in the corner while it “checks for new mail.” Sometimes it’s theatrical: the whole inbox goes gray and you can’t scroll, search, or open the message you were literally just reading, because the app decided the network is the boss now.

I have lived in that loop. Open app, wait. See spinner. Wait more. Network hiccups. Spinner again. Finally the list appears, you tap a thread, and the body loads with another spinner because the preview text was cached but the full MIME tree was not. Close the app in frustration. Reopen. Congratulations, you have been drafted into the Spinning Circle War.

Synesis exists partly because I got tired of fighting that war on my own inbox.

Local-first is a read order, not a manifesto

“Local-first” gets used like a bumper sticker. In practice, for mail, it means something boring and useful:

The UI reads from a database on your device. Sync is background work that updates that database. The network is not in the critical path for reading.

That’s it. No poetry required.

When you open Synesis, you’re not waiting for Microsoft or Google to bless your scroll position. You’re reading rows from SQLite: message list, folder state, bodies, flags, the search index. If you synced five minutes ago on bad hotel Wi-Fi, you still have the mail you synced five minutes ago on disk. If you’re on a train with no signal at all, you still have whatever you already pulled down. The app doesn’t pretend the cloud is the only place mail can exist.

Providers still matter. Synesis talks to Microsoft Graph for supported Microsoft 365 and Outlook accounts, and to IMAP/SMTP for other supported hosts, including Gmail via OAuth or an app password. Synesis doesn’t run a cloud mailbox. Your copy lives on your machine. When sync runs, it fetches changes and writes them into SQLite. When you search or archive a message, the UI responds immediately using local state; the server catches up when sync can reach it.

That ordering is the whole trick.

Why SQLite specifically

I could have picked a fancier embedded store. I didn’t, because mail is already a relational problem wearing a trench coat.

Folders are tables with relationships. Messages belong to threads. Flags and read state are columns you want to update without rewriting the universe. Search wants an index you control. SQLite is boring in the best way: self-contained storage, one file (or a small family of files), transactional writes, decades of battle scars, available everywhere I need Synesis to run.

It also keeps me honest about scope. If the UI depends on a piece of durable state, that state gets an explicit home in the schema: a migration and a column, not a hand-wavy “we’ll hydrate it later from the API.” If sync delivers a batch of messages, they land in the database before anyone updates a widget. The database is the contract between “sync did its job” and “the list view can render.”

Credentials stay out of that file on purpose. OAuth tokens and IMAP passwords live in platform secure storage, not in plain text next to your threads. SQLite holds mail state, not secrets.

What changes when the disk is home base

A few things that sound small and feel huge in daily use:

Scroll does not negotiate with the network. The list is local. Pagination is local. If you have 10,000 messages synced, you can scroll through them at 2 a.m. in airplane mode without performance theater.

Local search is yours. Build the index on device, query on device. You’re not sending every ordinary search string to a vendor’s opaque endpoint and hoping their relevance model agrees with you. What isn’t on disk yet isn’t in that index. Sync has to bring it home first.

Offline is a feature, not an apology. Not “offline mode” as a crippled subset. Just: the data you already have is available until sync brings more.

Sync failures degrade gracefully. If Graph or IMAP is unhappy, you still read what you have. Drafts and the outbox stay on the device until send can happen. The whole app does not get to hide behind one global spinner.

That last one is where the Spinning Circle War usually starts. Cloud-first clients often treat “cannot reach the server right now” as “the user may not interact with mail.” Local-first flips it: interaction is local; sync retries when the world cooperates.

The sync layer is still hard (I’m not pretending otherwise)

SQLite as source of truth doesn’t make IMAP IDLE or Graph delta tokens magically pleasant. It does make the failure modes legible.

Sync writes to the database. The UI observes the database. When those two concerns stay separated, I can fix a sync bug without redesigning every screen, and I can improve list performance without accidentally applying server state twice.

Is it more work up front? Yes. Is it work I would do again? Also yes, because the alternative is shipping an email client that feels fast on demo Wi-Fi and fragile everywhere else.

Where Synesis fits in the studio

Synesis is the first instrument in the Organon family far enough along to talk about without hand-waving: product page, privacy and terms documentation, real architecture choices. SQLite on disk is one of those choices, and it’s not going away because a slick webmail clone made spinners feel normal.

If you want the short version of what the app does and doesn’t do, the landing page is here: livebytes.net/synesis/.

Coming soon-ish

Fair warning: the next note wanders into AI territory. Not “10 models that will 10x your inbox,” just an honest look at what excites me about agentic tooling while I’m still the developer responsible for the SQLite schema and the IMAP edge cases. Good agents can help both Synesis and Phronesis; both also punish lazy prompts. That tension is worth writing about.

This is the longer take of July’s sticky-note version. Same argument, more of the how.

For now: mail on disk first, sync in the background, spinners demoted to optional garnish.

~ Trish

← All notes