Case Study

BrewBundle

BrewBundle

Overview

Setting up a new Mac is a day of tab hunting, half remembered tool names and installing one thing at a time. BrewBundle removes that. It pulls the entire Homebrew catalogue, over 16,000 formulae and casks, straight from the official API, sorts it into browsable categories and lets you search it instantly. You tick the packages you want into a basket that survives a refresh, and it compiles down to a single line you paste into Terminal. Sign in and you can save that bundle, name it and share it at a public URL so anyone can copy your setup. It is free, and nothing is piped from my server.

The Problem

Every time I set up a Mac, I lost most of a day to the same ritual. Open a dozen tabs, one per tool I half remembered needing. Work out which ones are brew install, which are brew install --cask, which want a downloaded disk image. Run them one at a time, waiting on each. Then discover three days later that I had forgotten ripgrep, jq and the font my terminal theme depends on.

Homebrew solved installation years ago. It is excellent. What it never solved is discovery and composition: working out what to install, getting the formula versus cask distinction right, and turning a wishlist into one repeatable action.

The existing answers only work for a narrow group. A hand maintained Brewfile is great if you already have one, but you have to have thought to create it before you needed it, and it rots the moment your tastes change. Dotfiles repos solve this beautifully for people who already live in the terminal and have the discipline to keep them current; they are useless to the designer who just wants Figma, Raycast and a decent editor. And "remembering what you had" is not a strategy, it is a guarantee that you will miss things.

The bottleneck was never installing software. It was deciding what to install and assembling the command. So that is what I made instant.

Constraints

I built this alone, so every decision had to earn its keep. No infrastructure I would have to babysit.

The catalogue had to be genuinely accurate and current. A package browser that lists things that no longer exist, or misses last month's additions, is worse than no browser at all. That ruled out any hand curated list.

The trust problem was the sharpest constraint. The whole product asks a stranger to paste a command into their terminal. People are right to be suspicious of that, and a tool built on "just run this" has an obligation to be careful in a way most sites do not.

It had to be free, with no account required to get value. Most visitors should never sign in, and should not have to.

And it had to be usable by people who are not terminal experts. Someone buying their first MacBook should be able to browse by category, recognise apps by name and logo, and get a working command without knowing what a cask is.

Approach

1. Build a boring, strict foundation first. Next.js with React Server Components, TypeScript in strict mode with noUncheckedIndexedAccess, environment variables validated and split across a hard server only boundary, and a pre-commit gate. Slow to start, fast for everything after.
2. Solve the catalogue data problem before building any UI. I worked out the fetching strategy first, because it constrained everything downstream.
3. Design a taxonomy. Raw Homebrew data has no categories, so I wrote an ordered rule engine plus a manual overrides file, and a script to audit the resulting distribution.
4. Ship the core loop with no backend at all. Browse, add to a basket, copy a command. Selection lived entirely in the browser. This was usable and useful before a single row existed in a database.
5. Add accounts only once saving was the obvious next thing. Usernames, GitHub sign in, and row level security policies written before the features that needed them.
6. Layer on saving and sharing, then a public showcase with a quality floor and full text search.
7. Run an adversarial security audit, and fix what it found before doing anything else.
8. Redesign the front door. The identity moved through a warm editorial phase, then a dark technical phase, then a light first flip, each recorded as a decision record.

Solution

  • A catalogue of every Homebrew formula and cask, over 16,000 items, fetched live from the four official Homebrew API endpoints, validated at the boundary so an upstream field rename fails loudly instead of silently blanking the UI.
  • A server side transform that slims the raw feeds, which run to tens of megabytes, down to fourteen fields per package, ships the result inside a prerendered page, and hands search to the browser. Typing costs zero network round trips.
  • Filtering by type, tap and category, with 22 categories driven by 33 ordered rules, live counts, and filter state reflected in the URL so a view is shareable.
  • A persistent basket that survives a refresh and follows you between the catalogue and the header.
  • The generated command itself, in two forms. The default is a single line: brew install for your formulae, then &&, then brew install --cask for your apps. Two invocations rather than one, because Homebrew does not accept mixed arguments in every version. The alternative is a brew bundle Brewfile inside a heredoc, which is idempotent and better for large setups but still pastes as one block. Both are produced by one pure, unit tested function.
  • Delivery is clipboard only. There is no script hosted on my server and nothing to pipe.
  • Saved bundles at their own public URL, public or private, editable, with per bundle social images, a copy a fork button, and a sitemap entry.
  • A community showcase with popular, recent and random sections, a quality floor enforced in SQL, and full text search across titles, descriptions and the package names inside each bundle.
  • Package names constrained to the Homebrew naming grammar in two places, the application schema and a database check constraint, so no shell metacharacter can reach a command that someone pastes.

Outcomes

I have no download counts or user numbers to report, and I am not going to invent any. The site is live, no analytics are wired up yet, and the public showcase currently holds a handful of bundles.

What I can say is what the tool now makes possible. A catalogue that was effectively unbrowsable unless you already knew the exact package name is now searchable by anyone in under a second, sorted into categories a non-developer can navigate. A setup that used to be an afternoon of sequential installs is a single paste. And a setup that used to live in one person's head can be handed to someone else as a URL, which is the part I actually care about: the new starter, the friend who just bought a Mac, the version of me on the next machine.

The security work has a concrete result. An audit found a stored shell injection: a signed-up attacker could have posted a bundle whose package name carried a payload, and any visitor copying the install line would have run it. That is now closed at the write boundary in two independent layers, with tests, and the reasoning is written down.

Reflections

The honest thread here is verifiability. I did the important half: nothing is piped from my server, package names cannot contain shell metacharacters, and the database enforces that independently of the application. But a cautious person still has to take my word that the line in the box matches the packages listed above it. I would add a way to prove that, rather than asking for trust.

Related, I validate the grammar of a package name but not its existence. A well formed name that no longer exists in Homebrew is accepted and simply fails at install time. Checking membership against the catalogue at save time is a small win I deferred and should not have.

There is a real duplication I would fix first: the public bundle page rebuilds the install one-liner with its own local function instead of calling the shared, tested generator. Two implementations of the single most important string in the product is exactly the kind of drift that bites later.

Documentation drift is the other lesson. I have a rule that docs ship with code, and it mostly held across eighteen decision records. But the root README is still scaffold boilerplate under a renamed title, one decision record describes a static curated bundle file that no longer exists, and a guide still claims the database is unused. The rule was right; enforcing it only by intention was not.

Finally, I wrote a careful measurement plan before launch and then shipped without instrumenting any of it. I have opinions about whether this works and no evidence. That is the next thing.