KOS applications are built around a model-first architecture where models expose backend abstractions of device state and capabilities. This approach fundamentally differs from traditional web architectures by organizing code around device characteristics rather than user interface concerns.
The Core Concept: Models as Architectural Bridge
What is a Model in KOS?
A KOS model is an architectural layer that bridges the gap between hardware systems and UI development. Models form a distinct layer – similar to a backend-for-frontend pattern but running in the browser – that provides performant, accessible state.
Models exist because hardware systems can’t necessarily maintain easy-to-access state in a performant manner for UI development. They expose backend abstractions of hardware concepts:
- Slice of device state: Specific state needed for UI features
- Backend abstractions: Simplified representations of hardware concepts
- Communication patterns: How to receive state updates
- Operational interface: Methods to interact with device capabilities
Key Point: Models are not UI components or React artifacts. They are standalone TypeScript classes that exist as an independent architectural layer between the backend and UI.
Synchronization vs. Replication
KOS models synchronize with backend state, they don’t replicate device functionality:
Physical Device: Pump calibrates, sensors read temperature, motors dispense
Backend System: Creates abstract models of pumps, ingredients, beverages
KOS Model Layer: Exposes these abstractions to enable UI development
UI Components: Display and interact with the exposed state
Example: An IngredientModel doesn’t replicate pump mechanics or fluid dynamics. It exposes:
- Ingredient availability status
- Expiration dates
- Assignments to physical pumps
- Properties needed for beverage creation
This allows UI developers to work with ingredient concepts without understanding the underlying hardware complexity.
Models as Domain Expertise Layer
KOS models can be created by either backend or frontend developers, but they require understanding of:
- Device protocols and communication patterns
- Backend abstractions and their meaning
- Operational constraints and safety requirements
- Business logic related to device operation
The decorator-based syntax makes this layer accessible to developers with various backgrounds:
import {
kosModel,
kosConfigProperty,
kosTopicHandler,
IKosDataModel,
KosCreationContext,
KosConfigProperty
} from '@kosdev-code/kos-ui-sdk';
const TOPIC_PUMP_STATUS = '/hardware/pump/status';
@kosModel({ modelTypeId: "industrial-pump", singleton: false })
export class IndustrialPumpModel implements IKosDataModel {
id: string;
// Configuration properties synchronized by framework
@kosConfigProperty({
path: "assembly:core:board:micro:pump:S4",
attribute: "nominalRate"
})
nominalRate!: KosConfigProperty<number>;
@kosConfigProperty({
path: "assembly:core:board:micro:pump:S4",
attribute: "maxFlowRate"
})
maxFlowRate!: KosConfigProperty<number>;
// Runtime state from backend
currentStatus: string = 'idle';
constructor(modelId: string, options: PumpOptions, context: KosCreationContext) {
this.id = modelId;
// Framework automatically keeps config properties in sync
}
// Subscribe to runtime state changes
@kosTopicHandler({ topic: TOPIC_PUMP_STATUS, websocket: true })
handleStatusUpdate(status: PumpStatus): void {
this.currentStatus = status.state;
}
}Examples of Model Thinking
Traditional Web App Thinking:
- UserProfile component
- ProductList component
- ShoppingCart state
- CheckoutForm validation
KOS Model Thinking:
- TemperatureSensor model (exposes readings, calibration settings from backend)
- DispenserNozzle model (exposes pour capabilities, selected beverage state)
- CoolingSystem model (exposes temperature state, compressor status)
- Door model (exposes locked/unlocked state from backend)
Model-First vs UI-First Design
UI-First Approach (Traditional)
In traditional web development, you often start with the user interface:
- Design the screens – What does the user see?
- Plan the interactions – What can the user click/type?
- Design the data flow – How does data move between components?
- Build the backend – What APIs support these screens?
Model-First Approach (KOS)
In KOS development, you start with understanding the backend abstractions:
- Understand the backend – What abstractions exist? What state is available?
- Model the abstractions – Create models that expose backend state
- Define communication – How does the backend report state changes?
- Build the UI – Create interfaces that visualize and interact with models
Device Abstractions Drive Structure
Traditional Web App Structure
src/
├── components/ # UI components
│ ├── Header/
│ ├── ProductList/
│ └── ShoppingCart/
├── pages/ # Route-based organization
│ ├── Home/
│ ├── Products/
│ └── Checkout/
├── services/ # API calls
├── hooks/ # Reusable logic
└── store/ # Application stateKOS Monorepo Structure
KOS applications typically use a monorepo structure where models are organized as separate, reusable packages:
workspace/
├── packages/
│ ├── device-models/ # Shared device models package
│ │ ├── sensor-models/
│ │ ├── dispenser-models/
│ │ ├── cooling-models/
│ │ ├── types/ # Device data structures
│ │ └── services/ # Device communication
│ │
│ ├── crew-serve-ui/ # UI for crew/operator interface
│ │ ├── components/
│ │ ├── pages/
│ │ └── package.json # depends on: device-models
│ │
│ ├── self-serve-ui/ # UI for customer interface
│ │ ├── components/
│ │ ├── pages/
│ │ └── package.json # depends on: device-models
│ │
│ └── admin-dashboard/ # UI for maintenance/admin
│ ├── components/
│ ├── pages/
│ └── package.json # depends on: device-modelsBenefits of Model Package Separation:
- Reusability: Same device models used across multiple UI applications
- Consistency: All UIs work with identical device representations
- Team Independence: Model developers work separately from UI teams
- Deployment Flexibility: Different UIs can be deployed independently while sharing device logic
Model Relationships Mirror Backend Structure
Backend Abstraction Relationships
The backend creates natural relationships between abstractions:
- A dispenser contains multiple nozzles
- A cooling system monitors temperature sensors
- A control panel manages multiple subsystems
Model Relationships Reflect This
KOS models use decorators to express these same relationships:
Container Relationships:
- DispenserModel contains NozzleModel instances
- CoolingSystemModel contains TemperatureSensorModel instances
- ControlPanelModel manages SystemModel instances
Dependency Relationships:
- NozzleModel depends on AvailabilityModel for pour options
- AssignmentModel depends on IngredientModel for valid assignments
Models Handle Backend Complexity
Backend State Complexity
Backend abstractions have complex states that don’t map to simple UI patterns:
- Asynchronous operations: Pour requests take time to complete
- Physical constraints: Can’t pour if ingredient unavailable or sold out
- Error conditions: Hardware failures, calibration issues
- Concurrent operations: Multiple sensors reporting simultaneously
Models Abstract This Complexity
Models handle backend complexity so UI components don’t have to:
Model Responsibilities:
- Track async operation progress
- Validate operations against constraints
- Provide access to backend state
- Coordinate multiple concurrent data streams
UI Responsibilities:
- Display current state clearly
- Provide controls for available operations
- Show progress and status information
- Handle user interactions gracefully
The Model Lifecycle
Traditional Component Lifecycle
Component Created → Props Change → State Updates → Re-render → UnmountKOS Model Lifecycle
Model Created → Load Baseline State → Subscribe to Updates →
Receive State Changes → Sync with Backend → Model CleanupModels have a backend-driven lifecycle where their behavior is tied to the availability and state of backend abstractions.
Benefits of Model-First Architecture
1. Clear Separation of Concerns
- Device state logic lives in models
- UI logic lives in components
- Communication logic lives in topic handlers and services
- No mixing of device concerns with presentation
2. Testability
- Models can be tested independently of UI
- Backend behavior can be mocked at the model level
- UI can be tested with mock models
- Integration tests can verify model-backend communication
3. Reusability
- Models represent backend abstractions, not UI-specific needs
- Same model can power multiple UI views
- State logic is independent of presentation choices
- Models can be shared across different applications
4. Maintainability
- Changes to backend behavior are isolated to models
- UI changes don’t affect backend synchronization logic
- New backend features require model updates, not UI rewrites
- Communication patterns are centralized
Model-First Design Process
Step 1: Backend Analysis
Questions to ask:
- What backend abstractions exist?
- What state is available from the backend?
- How does the backend communicate changes?
- What operations are exposed?
- What can go wrong?
Step 2: Model Design
Define:
- Model properties (backend state slice)
- Configuration properties (using
@kosConfigProperty) - Runtime properties (from topic handlers)
- Service functions (for operations and initial data loading)
- Topic handlers (state update subscriptions)
- Error handling (backend failures)
Step 3: UI Design
Build interfaces that:
- Visualize model state clearly
- Provide controls for model operations
- Show progress for async operations
- Handle model errors gracefully
Step 4: Integration
Connect:
- Models to backend through services and topics
- UI components to models through hooks
- Operations to user interactions
- Status updates to visual feedback
Example: Thinking Model-First with Shared Models
The Backend: Smart Coffee Dispenser Abstractions
Backend Exposes:
- Dispense operations (coffee, tea, hot water)
- Water temperature state
- Ingredient level tracking
- Maintenance status reporting
Shared Model Design (device-models package):
// packages/device-models/src/dispenser-model.ts
import {
kosModel,
kosTopicHandler,
IKosDataModel,
KosCreationContext
} from '@kosdev-code/kos-ui-sdk';
import {
fetchDispenserState,
startDispense,
type DispenserStateResponse
} from './services/dispenser-service';
const TOPIC_TEMPERATURE_UPDATE = '/dispenser/temperature';
const TOPIC_INGREDIENT_LEVEL = '/dispenser/ingredient-level';
@kosModel({ modelTypeId: "dispenser-model", singleton: false })
export class DispenserModel implements IKosDataModel {
id: string;
// State exposed from backend
currentTemperature: number = 0;
targetTemperature: number = 0;
ingredientLevels: Map<string, number> = new Map();
maintenanceStatus: MaintenanceInfo | null = null;
constructor(modelId: string, options: DispenserOptions, context: KosCreationContext) {
this.id = modelId;
}
// Load baseline state from backend
async load(): Promise<void> {
const [error, state] = await fetchDispenserState(this.id);
if (error) {
console.error('Failed to load dispenser state:', error);
return;
}
this.currentTemperature = state.currentTemp;
this.targetTemperature = state.targetTemp;
this.ingredientLevels = new Map(state.ingredientLevels);
this.maintenanceStatus = state.maintenance;
}
// Operations exposed to UI
async requestDispense(beverage: string, amount: number): Promise<void> {
const [error, result] = await startDispense(this.id, beverage, amount);
if (error) {
console.error('Dispense failed:', error);
return;
}
console.log('Dispense started:', result.operationId);
}
// Subscribe to backend state updates
@kosTopicHandler({ topic: TOPIC_TEMPERATURE_UPDATE, websocket: true })
handleTemperatureUpdate(update: TemperatureUpdate): void {
this.currentTemperature = update.current;
this.targetTemperature = update.target;
}
@kosTopicHandler({ topic: TOPIC_INGREDIENT_LEVEL, websocket: true })
handleIngredientLevel(level: IngredientLevel): void {
this.ingredientLevels.set(level.ingredientId, level.amount);
}
}Multiple UI Applications Using Same Model:
Crew-Serve UI (Operator Interface):
// packages/crew-serve-ui/src/dispenser-controls.tsx
import { DispenserModel } from "device-models";
import { useKosModel } from "@kosdev-code/kos-ui-sdk";
function OperatorControls() {
const dispenser = useKosModel(DispenserModel, "main-dispenser");
return (
<div>
<TemperatureControls model={dispenser} />
<ManualDispenseControls model={dispenser} />
<MaintenancePanel model={dispenser} />
<IngredientRefillIndicators model={dispenser} />
</div>
);
}Self-Serve UI (Customer Interface):
// packages/self-serve-ui/src/beverage-selection.tsx
import { DispenserModel } from "device-models";
import { useKosModel } from "@kosdev-code/kos-ui-sdk";
function BeverageSelection() {
const dispenser = useKosModel(DispenserModel, "main-dispenser");
return (
<div>
<BeverageButtons model={dispenser} />
<DispensingAnimation model={dispenser} />
{/* No maintenance or refill controls for customers */}
</div>
);
}Admin Dashboard (Maintenance Interface):
// packages/admin-dashboard/src/system-overview.tsx
import { DispenserModel } from "device-models";
import { useKosModel } from "@kosdev-code/kos-ui-sdk";
function SystemOverview() {
const dispenser = useKosModel(DispenserModel, "main-dispenser");
return (
<div>
<UsageStatistics model={dispenser} />
<MaintenanceSchedule model={dispenser} />
<PerformanceMetrics model={dispenser} />
<DiagnosticTools model={dispenser} />
</div>
);
}Benefits of This Structure:
- Same backend abstraction used across all three interfaces
- Consistent behavior regardless of which UI is used
- Independent deployment of different UI applications
- Shared testing of device models across all UIs
Mental Model Shift
From “What does the user want to do?”
To “What backend abstractions exist and how do they expose state?”
This shift changes how you approach every aspect of application design:
- Architecture: Organized around backend abstractions
- State management: Reflects actual backend state
- User interactions: Send commands through service functions
- Data flow: Driven by backend state changes
- Error handling: Based on backend failure modes
- Testing: Focused on synchronization accuracy
Model-first architecture creates applications that feel responsive and reliable because they accurately represent the backend abstractions of the physical systems they expose.
Key Takeaways
Models are an architectural layer – They bridge the gap between backend systems and UI needs, similar to a backend-for-frontend pattern running in the browser.
Models expose backend abstractions – They don’t replicate all device functionality, just the state slices needed for UI development.
Models synchronize, not replicate – They maintain sync with backend state through configuration properties, service functions, and topic handlers.
Reusability is fundamental – Same models can power multiple UI applications (operator, customer, admin) with consistent behavior.
Model-first design – Start by understanding backend abstractions, then build models to expose them, then create UI to visualize and interact with them.
