# Macos Blueprint

> Use the macOS app blueprint to shape new native menu bar apps under ~/apps/macos. Use when deciding a menu bar app's data source, whether it needs the credentials/networking/oauth/icon-render modules, its distribution path (Developer ID direct vs Mac App Store), how the menu and settings are structured, or whether an idea needs native work beyond the menu bar (sidebar/full window). Pairs with the `macos` CLI skill for commands.

- Skill: `iannuttall/macos-blueprint` (Agent Skill)
- Install (CLI): `npx skillmds@latest add iannuttall/macos-blueprint`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iannuttall/macos-blueprint/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: iannuttall (https://skillmd.com/u/iannuttall)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iannuttall/macos-blueprint

---


# macOS menu bar app blueprint

Architectural decisions for apps built on `~/blueprints/macos`. For commands, use the `macos` skill.

## The shape every app shares

- **SwiftPM, three targets:** `__App__Core` (portable logic, tested via a CLI), `__app__cli`
  (headless), `__App__` (thin AppKit+SwiftUI UI). Put logic in Core; keep the app target thin.
- **AppKit `NSStatusItem` + native `NSMenu` with SwiftUI rows** hosted in `NSMenuItem.view`. Not
  `MenuBarExtra` — that can't draw live icons or show multiple items, and boxes you in.
- **Modern Observation** (`@Observable`/`@State`/`@Bindable`), injected `UserDefaults`, no
  `ObservableObject`.
- **Developer-ID-direct** distribution: notarized zip → Sparkle auto-update → Homebrew cask.

## Decide the data source

| App idea | Data | Modules to enable |
|---|---|---|
| Static/computed (clock, system stat, timer) | none / local compute | core only |
| Reads a public API (weather, prices, status) | URLSession + cache | `networking` |
| Reads an authed API (GitHub, a SaaS) | API + stored token | `networking` + `credentials` (+ `oauth` if OAuth) |
| Local prefs/state only | `UserDefaults` (built in) | core only |
| Live indicator in the menu bar (progress, count) | any of the above | add `icon-render` |

`macos new <slug> --api` enables `networking` + `oauth` + `credentials` up front. Otherwise add a
module later with `macos modules update <slug> <module>`.

## Credentials

If the app stores a token/key, use the `credentials` module's `TokenStore` (Keychain in release,
file in debug — so dev and agents never hit a Keychain prompt). Never roll your own `SecItem` calls;
never log tokens. For OAuth, pair `oauth` (PKCE + loopback callback) with `credentials`.

## Menu & settings

- Menu content is a `MenuDescriptor` value tree — add sections/entries there, not ad-hoc in the
  controller. Keep it pure so it stays unit-testable.
- Settings is a tabbed `Settings` scene; add a pane per concern, reuse `SettingsSection` /
  `PreferenceToggleRow`. Persist via `SettingsStore` (injected `UserDefaults`).

## Distribution

- **Default: Developer ID direct.** Best for open source — no sandbox restrictions, Sparkle
  auto-update, `brew install --cask`. This is fully built.
- **Mac App Store:** designed in `references/app-store-connect.md`, lands in v1.1. Set
  `distribution: "mas"` in `app.config.json` when ready; it requires app-sandbox entitlements and a
  different upload path. Choose MAS only if you need its discovery/billing; it costs you the
  non-sandbox freedoms.

## When the menu bar isn't enough

A full window with a sidebar (the eventual "proper app" track) is additive: the same Core + the same
`Settings` scene, plus a real `WindowGroup` with `NavigationSplitView`. Anything needing private
entitlements (FamilyControls, screen recording, accessibility automation) is a meaningfully larger
native effort — flag it early; it's not a menu-bar-afternoon.

## Don't

- Don't reach for `MenuBarExtra`, `ObservableObject`, or `@AppStorage` — they're off-pattern here.
- Don't add dependencies casually; the baseline is Foundation + Sparkle.
- Don't put identity/distribution facts anywhere but `app.config.json`.

