The nine apps my family runs on are their own story. This is about what happened after I built them in four different frameworks and got tired of their looking like nine unrelated weekend projects.

They’re named todo. list. shop. fit. golf. watch. home. trips. and vault. — lowercase word, then a period. One is Preact inside Astro, one is Astro with server rendering, one is a Vite/Svelte PWA, one is a static site, and a couple are containers I’d rather not describe in polite company.

They look like one product anyway. The mechanism is smaller than you’d think.

The rule that does most of the work

I stole this from the email side of my infrastructure, where it’s written down explicitly:

The bones are identical across every product. Each product gets its own identity through accent color + signature element — never through type stack or layout.

The reader recognizes the shape instantly, then recognizes which one by its color.

Two sentences, and it’s the entire design philosophy. Every temptation to make shop. feel different from todo. — its own font, its own layout, rounder corners — is the temptation to make nine hobby projects. Identity is allowed exactly one channel. Everything else is shared.

Consistency is the cheapest credibility there is. Nobody looks at a family of apps that share a wordmark, a type stack, and a palette and thinks “hobby project.” They think somebody’s in charge.

The visual system

The name is the logo. A lowercase word and a period, set in JetBrains Mono at weight 500, where the period is the only colored thing on the screen: #1F4DC2, a flat cobalt blue. That’s it. No mark, no glyph, no wordmark drawn by a designer. todo. is the logo of todo.

Monospace works for a wordmark for a reason people don’t usually say out loud: the period — normally the least important character in typography — occupies a full character cell. In a proportional face a period is a crumb. In a mono face it’s an event. Making it the accent color turns punctuation into a brand.

The palette is paper, not app. White surface, near-black ink with a cool tint (#14171D), a stepped gray ramp for rules and metadata, cobalt for accent, and three status colors. No gradients, no drop shadows, 2–4px border radius at most, hard hairline rules between sections.

The chrome is an IDE. Every app carries a fixed tabbar across the top and a modeline across the bottom — the two pieces of furniture that make a code editor recognizable from across a room. It’s a deliberate metaphor: these are tools for someone who lives in a terminal, so they’re shaped like one.

Type has a hard job split. Mono for headlines, chrome, labels, timestamps, and numerals. Inter for anything a human reads as a sentence. That line exists because of a real bug — during a migration, the token that used to resolve to a serif face silently started resolving to JetBrains Mono, and every call site that had used it for prose rendered paragraphs in monospace. The worst casualty was a 1,200-word email, set entirely in code font, shipped to my inbox looking like a ransom note. The rule got written down that day: mono is for kickers, metadata, and display numerals; Inter is for anything read as sentences.

The icon template is a specification, not a vibe

It’s the piece of the system I’d defend in an argument, because “make the icons consistent” is the kind of instruction that quietly produces nine different icons.

The app icon is a white rounded square. A single window dot in the top-left corner. Four tabbar bars along the bottom-left — the first one cobalt, the rest at 25% ink. And the lowercase wordmark, ending in the cobalt period. It’s the IDE chrome again, at 512 pixels.

The rule that makes it hold together across names of different lengths:

Hold optical width constant — about 64% of the icon width — not font size.

So fit. (three glyphs plus the period) sets at font-size 31, and todo. and shop. (four plus the period) set at 24. Letter-spacing is −0.035em. The baseline sits at y = 50 + 0.2494 × fs on a 100-unit viewbox. Every raster is a render of the SVG through rsvg-convert — never a hand-edited PNG, because hand-edited PNGs are how a family drifts.

Favicons are a separate problem and get a separate answer. The full icon at 32px is illegible mush, so the favicon is just the first letter and the blue period on a transparent background — t. s. f. The letter form is deliberate, not a simplification.

The consistency check that embarrassed me

When the newest icons were done, I ran a lineup test: an unlabeled row of icons — the two new candidates alongside the existing, shipping fit. and shop. — in front of a grader who had never seen any of them, with one question: which ones don’t belong?

It couldn’t find the new ones. It flagged the real, live, already-shipped icons as the outliers instead.

Pixel forensics confirmed it. shop. had square corners and was missing its tabbar bars entirely — it had never actually conformed. fit. had a wordmark 25–40% oversized with the baseline in the wrong place. Both were invisible at thumbnail size and obvious at 192px, which is exactly how design debt survives: it’s only wrong when you look properly.

The note I wrote afterward is the useful part: the family template is tighter than the family itself. Writing the spec doesn’t conform the apps to it. Somebody has to go audit the ones that shipped before the spec existed.

The technical system

One file is the source of truth:

:root {
  --paper:#FFFFFF; --paper-deep:#F4F4F5; --paper-tint:#FAFAFA;
  --ink:#14171D; --ink-soft:#2D313A; --muted:#6E727A; --muted-2:#A1A4AB;
  --rule:#E6E6E8; --rule-strong:#CFCFD2;
  --accent:#1F4DC2; --accent-soft:#5A77CC; --accent-bg:#E1E9F8;
  --green:#2C7A4B; --amber:#B26A00; --red:#B23A2A;
  --mono:"JetBrains Mono",ui-monospace,"SF Mono",Menlo,monospace;
  --sans:"Inter",ui-sans-serif,system-ui,-apple-system,sans-serif;
}

A drift audit checks four things per app, and they’re deliberately mechanical enough to run without judgment: no hardcoded hex that duplicates a token value, accent is cobalt and any orange is drift, headlines use mono and body uses sans with no stray fonts, and the app embeds the fleet switcher in its top chrome.

Editing that tokens file doesn’t silently reformat nine apps. It enqueues one backlog item per other app — “conform <app> to tokens vN” — and shared-foundation changes are marked pull-request-only, never auto-shipped. A design system that can change nine apps while you sleep is not a design system, it’s a liability.

        tokens.css  ──────────────┐
        (one file, the language)  │

   ┌──────────┬──────────┬──────────┬──────────┐
   │  todo.   │  shop.   │  watch.  │  vault.  │   … nine apps,
   │ Preact/  │  PWA in  │  Astro   │  Astro   │     four stacks
   │  Astro   │  docker  │   SSR    │  static  │
   └──────────┴──────────┴──────────┴──────────┘
        each adds: its name, and nothing else

The switcher is the trick that makes stack diversity survivable. Every app carries a small launcher to the rest of the suite, and it’s a plain Web Component — custom element, shadow DOM, no build step, no bundler, no framework:

class FleetSwitcher extends HTMLElement {
  connectedCallback() {
    const current = this.getAttribute("current") || "";
    const root = this.attachShadow({ mode: "open" });
    const links = APPS.filter(a => a.id !== current)
      .map(a => `<a href="${a.url}">${a.label}</a>`).join("");
    // ...
  }
}
customElements.define("fleet-switcher", FleetSwitcher);

One <script src> tag works from Astro, Svelte, Preact, or a raw HTML file. Its styles reference the fleet tokens with inline fallbacks (var(--mono, ui-monospace, monospace)), so it renders correctly even inside a host app that hasn’t adopted the token file yet. Shadow DOM means the host app’s CSS can’t reach in and break it.

Two small decisions in there I like. It filters out the app you’re currently in, because a self-link in a nav reads as a duplicate. And it’s preceded by a divider and a tiny muted “apps” label, so it reads as the rest of the suite rather than as this app’s own tabs.

PWA caches will serve you last month’s app

The icon rollout taught me more about caching than about design.

Browser favicons and installed-app icons are separate cache surfaces, and updating the PNG is not sufficient for either. The checklist that finally worked:

  1. Version the favicon, Apple touch icon, and manifest links in the page HTML.
  2. Version every icon src inside the web manifest.
  3. Check the generated production HTML — the PWA plugin will happily auto-inject an unversioned manifest link even when your source HTML looks correct.
  4. Fetch the live manifest and every icon URL after deploying.
  5. Hash-compare at least one live production icon per app against the local source file.

Four of the apps needed a post-build transform to rewrite the auto-injected link with a version query. The final audit checked all nine apps: HTML 200, every manifest icon 200, and the live primary icon byte-identical to its source.

Two platform quirks worth knowing. Chrome keeps favicons in a store of their own, so a hard refresh or a fresh tab is required. And iOS bakes both the icon and the label at add-to-home-screen time — an installed app will keep showing last year’s icon forever until you delete and re-add it. That one isn’t a cache you can bust; it’s a cache you have to ask a human to clear.

What’s still not right

vault. never got the letter treatment — it still has its own three-bar favicon. It was considered and parked, which is how most design debt actually happens.

trips. is a deliberate exception to the icon family, with richer editorial artwork, because that product’s identity is genuinely different. The generator has a flag for it. Deliberate exceptions are fine; the danger is that a flag makes it easy to add a second one.

And under the hood there’s almost nothing shared. Four frameworks, several deployment mechanisms, and a lot of duplicated plumbing. The design system creates the appearance of a coherent product on top of a genuinely incoherent foundation—which has been the right order for this little fleet. The look is what everybody touches every day. The plumbing is what I touch.

If I build a tenth app, the tokens file comes first. It’s the cheapest thing in this entire post and the reason nine unrelated apps feel like software instead of a folder of experiments.