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

The Domain Model

~15 min · firelink, domain-model, capability, types

Level 0Cold Ash
0 XP0/32 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Four nouns, and not one of them is a raw command."

The Four Objects

Firelink's whole world is four typed objects. FamilyMember is a repository or product identity — an id, display metadata, a lifecycle status, repository identity, host affinity, and zero or more surfaces and capabilities. LaunchSurface is a user-facing service or app a member produces — a stable target id, its kind (web or native-app), a label, and either context-specific URLs or a native bundle id. Capability is a typed operation the hub may offer for a member. ObservedState is the read-only facts measured during census.

Notice what is not here: there is no object for 'the command the user typed.' Operations are never raw strings floating through the system. They are typed capabilities, named and resolved by policy. That single absence is a security decision baked into the data model.

Capability Is Resolved by the Backend, Never Inferred

A capability is a value like inspect-git, commit-and-push, birth-resume, deploy-native for a specific adapter, or restart-service for a specific service id. The critical rule: capabilities are resolved by backend policy, and the frontend never infers an operation from a non-null label or a visual card type. The UI doesn't look at a card, decide 'this looks like an app,' and show a Deploy button. The backend says 'this member has these capabilities, and these are disabled with these reasons,' and the UI renders exactly that. A member with no deploy capability never even renders the selector.

Observed State Knows What It Doesn't Know

ObservedState is measured, not declared, and it carries its own honesty. Every observation records a measured_at timestamp and a probe source, and — this is the subtle part — it keeps unknown separate from stopped and missing. 'I couldn't tell' is a different fact from 'it's down' and from 'it isn't installed.' A model that collapses those three into one boolean will eventually report a probe failure as an outage, or an outage as 'fine.' Firelink refuses to guess in that gap.

Model operations as typed capabilities resolved by policy, never as strings the UI infers. When the set of possible operations is a closed, typed list resolved server-side, the client cannot invent an action the backend didn't authorize — the type system becomes the permission boundary.

Code

Four objects — and Capability is a closed, typed set·typescript
type Lifecycle = "scaffold" | "active" | "attic" | "legacy";

type Capability =
  | { kind: "inspect-git" }
  | { kind: "commit-and-push" }
  | { kind: "birth-resume" }
  | { kind: "deploy-native"; adapterId: string }
  | { kind: "deploy-tree"; adapterId: string }
  | { kind: "restart-service"; serviceId: string };
// There is NO { kind: "run"; command: string }. Raw commands aren't in the model.

type ObservedState = {
  measuredAt: string;                 // every fact is timestamped
  source: string;                     // which probe produced it
  service: "running" | "stopped" | "missing" | "unknown";  // 'unknown' != 'stopped'
};

type FamilyMember = {
  id: string;
  lifecycle: Lifecycle;
  surfaces: LaunchSurface[];
  capabilities: Capability[];         // resolved by BACKEND policy, not the UI
  observed: ObservedState;
};

External links

Exercise

Look at a UI you've built or used that shows action buttons per item. Does the client decide which buttons to show (by inspecting the item), or does the server hand it an authorized list? Find one button whose visibility is inferred client-side. Describe the attack or bug where that inference is wrong — a button shown that shouldn't be, or an action taken the server never authorized — and how a server-resolved capability list closes it.
Hint
Client-inferred buttons fail in two directions: showing an action that then errors, or worse, letting a crafted request trigger an action the UI 'hid' but the backend still accepts. Server-resolved capabilities fix both because the authority lives in one place.

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.