← back to work
case study · macOS app

Chorus: gathering the web apps you never close into one window that stays quiet.

Twenty services in one window, and most of the design spent on the harder half: unread counts that stay inside their space, a switch over every badge and notification, and two accounts of the same service sealed from each other.

The Chorus GitHub repository shared in a design cohort’s Slack channel: a repo card reading 'nicojan / Chorus, a native macOS app that unifies web services into a single window with sandboxed sessions per account,' with 22 stars and members reacting and offering to test it.
Shared in a design cohort, it was picked up and tested by people I’d never met.
Client A personal project, for people who keep a dozen web services open all day
Sector Software, consumer desktop tools
My Role Product and interaction design, native build, sole author and maintainer
Team Solo, prototype to release pipeline
Duration April to July 2026
Methods Interaction design, notification and attention design, session isolation and privacy-boundary design, native macOS build, release engineering, community bug triage
Outputs A shipped, notarized, self-updating macOS app · open source

The tools you never close are the ones that never stop asking for you.

Mail and team chat, a doc or two, a calendar you keep half an eye on. They sit open all day, and any one of them can interrupt whenever it likes.

Those applications live in a scatter of browser tabs and menu-bar apps. No single interruption is the problem. The tax is a dozen open things at once, each of them holding a claim on you.

Apps that gather your services into one window already exist: Rambox, Franz, or a browser with enough tabs open. Most tidy the services and leave the noise, so the unread counts pile up in one place instead of several. Chorus started from the other end. Collecting the services was the easy part. Keeping the collection calm took the rest of the work.

Chorus running on macOS: several web services gathered in one window, with a service-switcher rail on the left, Slack open to its Activity list and a channel, and message content blurred for privacy.
The app itself One window, a strip of services across the top, and a rail to move between them. Each one is loaded and none of them is allowed to interrupt.

I built Chorus to quiet my own desktop, then designed it into a product.

It started in April 2026 as a rough shell that could show a few services in one window. I designed and built everything it became.

That shell had the first sketches of spaces, badges, and keyboard shortcuts in it. I set it down for a few weeks, came back in June, and built the rest, down to the release pipeline that signs and notarizes each build before it ships. I have maintained it in public since, folding in fixes as people put it to use.

Spaces group your services the way your day already splits them.

Work in one space, personal in another. One is active at a time, and unread counts add up per space instead of into a single undifferentiated pile.

A space is a named group of services: a work space with team chat, mail, and a document tool; a personal space with the messaging apps friends actually use. Only the active one is on screen, so the window holds the six things this part of the day needs rather than the 20 the whole day might. A personal message never lights up the work space, and the reverse holds too.

Spaces Each space carries its own unread count. The active space shows only its services, so the part of your day you are in is the only part on screen.

A wall of services must not become a wall of noise.

Every service arrives assuming it may badge you and notify you at will. The design gives you the last word over both.

Chorus gives you a switch for each. A message still arrives. Whether it reaches you is a decision you have already made.

Two switches rather than one looks like indecision until you follow where the two things come from. An unread count is something Chorus goes and reads off a page on a timer. A notification is something the page hands over the moment it happens. They arrive by different routes, only one of them is ever written down, and a single control over both would have to be wrong about one of them.

Quiet hours make the decision on a schedule, wrapping correctly past midnight so a window like 10 p.m. to 7 a.m. behaves the way you mean it. Services you are not looking at hibernate, so 20 open things do not cost 20 things' worth of memory, battery, or interruption.

Two pipelines, one mute Counts are fetched and kept; notifications are caught in passing and kept by nobody. The third row is the one worth reading twice: to know whether something you are not looking at has unread mail, Chorus has to load the page itself, because the server will not tell it.

The keyboard beats the mouse a hundred times a day.

Press ⌘K, type a few letters, and Enter takes you there.

The list narrows on a space name as readily as a service name, so the same few keystrokes reach either. For an app you keep in front of you all day, reaching for the mouse to change context is friction you feel a hundred times.

Quick switcher A recreation of the ⌘K switcher. Type, and the list narrows across service names and spaces at once; the whole interaction lives on the keyboard.

Two accounts of the same service should never see each other.

A second Gmail gets its own sealed session, so cookies and unread counts never bleed from one account into the other.

People run two of the same thing: a work Gmail and a personal one, two Slack workspaces. Inside a single browser those share cookies and sign-in state, so a second account becomes a chore of constant logouts, or a leak waiting to happen. In Chorus the two sit side by side, each signed in, and neither has to be logged out to reach the other.

Session isolation Each service is its own sealed store. A second account of the same service is fully separate, so nothing crosses the seam.

Three times, I cut the feature that demos best.

Cutting them kept the app light, and honest about what it is.

The reasoning was the same each time. An app you keep open all day is judged on what it costs you while you are not thinking about it, so the lighter path wins even when the heavier one reads better in a feature list.

Three features cut, and what went in instead
what I cutwhy the obvious version was wrongwhat shipped
heavy ad and tracker blocking A roster of messaging apps has little to block, and doing it properly means fetching and compiling large blocklists inside a tight memory budget. Steep cost, thin benefit. Nothing, at first. A lighter domain-level version earned its way in later.
shrinking the base font to fit more on screen It breaks fixed-size images and per-service layouts, and small-by-default is the opposite of accessible. It was filed as an accessibility feature and would have cost accessibility. A uniform zoom that scales a whole service, larger or smaller.
bundling a browser engine Shipping a second engine buys control over rendering and pays for it in download size and memory, plus a security surface I would then be on the hook to patch. The system’s own web engine, patched by the operating system.
What restraint cost and bought The middle column is the one that matters: each of these was cheap to add and expensive to keep, and the font-size one would have been shipped under the name of the very thing it damaged.

Shipped 2 July 2026, signed and notarized from day one.

Then improved steadily as real users, people I had never met, found what I had missed.

A Sparkle update feed went in with that first version, so it patches itself without a trip to a download page. The releases since have been mostly repair and refinement.

Some of those fixes came from strangers. A user on Reddit reported a crash on launch that struck after deleting a space, which traced back to a stale reference left in saved data. Version 1.5.1 stopped new deletions from causing it, and 1.5.2 repaired stores already broken by the earlier bug, backing up the data file first. Shipping in public means strangers hit the bugs, and answering them is part of the deal.

  1. v1.0.0 · 2 July 2026

    First public release. Signed, notarized, and self-updating from the first day.

  2. v1.4.0 · 9 July 2026

    Reader mode, automatic dark mode, and a light domain-level ad and tracker blocker, the restrained version of the feature cut earlier.

  3. v1.5.0 · 12 July 2026

    Move a service from one space to another with a right-click.

  4. v1.5.2 · 13 July 2026

    Fixed a launch crash a stranger reported, and stopped badge counts leaking between two accounts of the same service.

Release cadence A few of the public milestones. Most releases since have been repair and refinement in response to real use.

The bug I regret broke the app’s main promise.

Badge counts leaked between two accounts of the same service. That is exactly the boundary the product sells, and I caught it late.

It shipped because my testing ran mostly on a single account per service, so the exact case the feature exists for went unexercised until someone hit it. I fixed it in 1.5.2, but the lesson is about order. When a product’s core claim is a boundary, the first test should try to cross it.