C.W.K.
Stream
Lesson 04 of 04 · published

A Repository Is Not a Launch Target

~12 min · firelink, domain-model, launch-surface, deep-linking

Level 0Cold Ash
0 XP0/32 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"You don't open a repository. You open the thing a repository produces — if it produces one."

Member and Surface Are Different Nouns

It's tempting to think 'the family is a list of repos, and launching means opening a repo.' Firelink refuses that shortcut. A FamilyMember is a repository or product identity; it owns zero or more LaunchSurface objects, where a surface is an actual user-facing thing the member produces — a WebUI, a native app. The launcher opens surfaces, never repository internals. Examples read cleanly: a Pippa WebUI surface, a Firekeeper menu-bar-app surface, an Ember engine-UI surface. The repo is the member; the openable thing is the surface.

Some Members Have No Surface at All

This distinction earns its keep at the edges. A shared kit or a workshop repository might produce nothing you can launch — it's a library other siblings vendor, or a batch tool with no server. Firelink still shows that member in the Web Hub (it exists, it has Git state, it matters), but it does not invent a fake launcher target for it. No phantom 'Open' button that does nothing. If a member has no surface, the launcher simply has nothing to offer for it, and that's the honest answer.

'Open' Is Context-Aware, Never Blind

Because a surface is a real thing and not just a URL string, opening it is context-aware. The same web surface resolves to localhost on the office loopback, to a LAN address on the local network, and to a Tailscale HTTPS address from the tailnet. There is deliberately no context-blind 'primary' URL that the UI falls back to — a link rendered on a card follows the same client-aware resolver as the main Open action. The surface knows how to be reached from wherever you are, instead of pretending one address works everywhere.

Separate the thing you manage (the member) from the thing you open (the surface). A member can have many surfaces, one, or none. Modeling them as one noun forces you to invent fake launch targets for surfaceless members and blinds you to the fact that one member can expose several openable things.

Code

Member owns surfaces; surfaces are what open·typescript
type LaunchSurface = {
  targetId: string;              // stable, e.g. "cwkPippa:webui"
  ownerId: string;               // the owning FamilyMember
  kind: "web" | "native-app";
  label: string;
  // web: context-specific URLs (localhost / LAN / tailnet), resolved per client
  // native-app: a registered bundle id
};

type FamilyMember = {
  id: string;
  surfaces: LaunchSurface[];      // ZERO or more — a kit may have none
  // ...identity, lifecycle, host affinity, capabilities
};

// A workshop repo with no server:
//   member.surfaces === []  ->  shown in the Web Hub, NO launcher target invented.
// The launcher opens surfaces. It never opens a member, and never fakes one.

External links

Exercise

Take a system that lists 'things' and lets you open them. Does it conflate the managed thing with the openable thing? Find a case where one managed item should expose two openable surfaces (e.g. a service with both a dashboard and an API console), or where an item has nothing to open at all. How would a member/surface split represent both honestly, without a fake 'Open' that does nothing?
Hint
The conflation shows up as either a dead 'Open' on something unlaunchable, or a single item you wish you could open two different ways. Both disappear once 'what I manage' and 'what I open' are separate nouns.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.