Why Iris feels instant
The local-first inbox: what WhatsApp Web can teach you about what NOT to store locally, the bounded cache Iris keeps on your device, delta polling, drafts that survive reloads — and the list of things we deliberately never cache.
Open the Iris inbox as a returning agent and there is no skeleton screen. No shimmer, no spinner, no three-beat pause while a list assembles itself. Your conversations are just there, painted from the first frame, and a moment later they're also fresh. This post is about the architecture behind that — and, just as much, about the things we deliberately chose not to build.
Two ways to build a "local" inbox
When people say an app "works like a native app," they usually mean one of two very different architectures.
The WhatsApp Web model: the client owns the data. WhatsApp is end-to-end encrypted, which means the server cannot render your message history — it only ever sees ciphertext. So the client has no choice: it must hold the entire store locally, sync every device pairwise, and migrate that store carefully across versions. That's why linking a new device replays history for minutes, and why the browser tab quietly owns gigabytes. For WhatsApp this is the right trade — it's forced by the encryption model, and it buys real privacy.
The Telegram/Slack model: the server owns the data, the client keeps a bounded cache. The server is the source of truth and can render anything on demand; the client holds just enough recent state to paint instantly and sync cheaply. New device login is instant. The local footprint stays small. The cache is disposable by design — lose it and you've lost nothing but a few milliseconds on the next open.
A support inbox is not end-to-end encrypted — your team's whole job is that the server can read the conversations, route them, report on them, and let five teammates see the same thread. So carrying WhatsApp's client-owned-store burden would buy us nothing and cost us everything that makes it hard: sync conflicts, migration bugs, unbounded storage. Iris is built on the second model, deliberately.
What Iris keeps on your device
The inbox keeps a small IndexedDB store per workspace and agent (the database is literally named iris.<workspace>.<agent> — more on why below). Three things live in it:
- The recent conversation list, per channel (chat and email) — the default view's rows, up to 200 per channel.
- The newest page of threads you've opened — the last 50 messages of each, nothing older.
- Your drafts — the half-written reply in any conversation's composer.
That's the whole inventory. On a cold open — new tab, hard reload, morning login — the last-known list paints immediately from disk while a fresh copy loads behind it. Stale-while-revalidate: you read the (seconds-old) truth while the current truth arrives, and the swap is invisible unless something actually changed. Opening a recent thread works the same way — the cached copy paints instantly, and the live fetch replaces it wholesale. We never merge a cached thread with a live one; a cached copy is display-only, marked as such internally, and discarded the moment real data lands. Merging two versions of a conversation is how you get phantom messages, and we'd rather repaint than reconcile.
What we refuse to cache
The interesting engineering decisions are the refusals — the places where a cache would be easy to add and quietly wrong:
- Filtered views are never cached. A filter is a query — "unassigned billing threads breaching SLA" — and a stale answer to a query isn't slightly old, it's incorrect in ways you can't see. Only the plain default list is cached; every filtered view hits the server.
- Unread counts are never cached. A stale badge number is worse than a briefly absent one — it tells you a lie with confidence.
- Email HTML bodies are stripped from cached copies. Rich inbound email can be hundreds of kilobytes of somebody else's markup. The cached copy paints the text; the live fetch restores the rich rendering about one round-trip later.
- Tickets aren't cached at all in this first version. They're lower-traffic and higher-stakes; they can wait for the server.
The principle: cache what makes the paint fast, never what makes a decision. Anything an agent might act on — who's assigned, what's breached, what's unread — comes from the server, every time.
Polling that mostly says "nothing"
Realtime updates arrive over the socket, instantly, as they always have. But every inbox needs a safety poll for the events that slip past — and the naive version refetches the full list every few seconds, forever, mostly to learn that nothing happened.
Iris's steady-state poll instead asks a delta question: "anything newer than the newest thing I hold?" — a since cursor riding the same index that serves the list. At steady state the answer is an empty array, which is about as cheap as an HTTP response gets. Only when something actually changed does the client run one full list fetch, which reconciles through the exact same merge path every other refresh uses — zero new merge logic, zero new edge cases. And because some changes don't move the cursor (a status flip, say), every sixth tick and every window re-focus runs the full fetch regardless. Trust, but re-verify on a schedule.
Drafts that survive
The most human part of the system: the reply you were halfway through typing when a higher-priority thread interrupted you. Iris persists drafts per conversation — switch threads, reload the tab, lose your connection on a train — and the half-typed reply is there when you come back. Two safety properties are absolute: a draft is only ever restored into an empty composer (it will never overwrite something you're typing), and nothing auto-sends, ever. Offline compose in a support tool should mean "your words are safe," never "your words went out without you."
Bounded on purpose, wiped on logout
The cache has hard edges: about 200 recent conversations per channel, the newest 50 messages per opened thread, a 14-day window, LRU eviction beyond that. Everything older simply loads from the server on demand — which, in the Telegram/Slack model, is not a degraded path but the normal one. The cache never needs a migration strategy, because a cache you can afford to lose is a cache you can afford to version-bump and wipe.
And it is wiped — completely — on logout. Support agents share computers more than any software vendor likes to admit; the per-workspace-per-agent database is deleted when the session ends, so nothing of one agent's queue lingers for the next login. That's also why the store is scoped the way it is: two agents on one machine never touch each other's cache even mid-session.
The boring conclusion
None of this is novel computer science — it's a bounded cache, a delta cursor, and a list of things we refused to cache. But that's rather the point. The shared inbox is a tool people live in all day, and at that duty cycle, latency isn't a metric — it's a mood. The architecture's job is to make the fast path instant, keep the truth on the server, and be so unambitious about local state that it can never betray you.
Speed you can trust is mostly restraint you can't see.
Put this playbook to work.
Create a workspace, paste one snippet, publish a few articles. Free to start — live before your coffee cools.