# Architecture Deep Dive

> Applies to: ChengOS v0.1.0+ ｜ Last verified: 2026-08-12 ｜ Source: `crates/cheng-core/src/ports/mod.rs`, `crates/cheng-core/src/ports/executor.rs`, `crates/cheng-engine/src/executor`, `chengflow/CLAUDE.md`

ChengOS's backend is a **hexagonal architecture** (ports and adapters) with domain-driven
design. That is not a slogan on this project — it is enforced by the crate graph: `cheng-core`
has no database, no HTTP, and no filesystem dependencies, so a domain rule cannot quietly reach
for a connection pool.

## The layers

```text
┌─────────────────────────────────────────────┐
│              Domain (cheng-core)            │
│  ┌─────────────────────────────────────┐   │
│  │  Business logic & domain models     │   │
│  └──────────────┬──────────────────────┘   │
│                 │ uses                      │
│  ┌──────────────▼──────────────────────┐   │
│  │  Ports (traits)                     │   │
│  │  WorkflowRepository, ExecutionRepo, │   │
│  │  Executor, EventPublisher, …        │   │
│  └─────────────────────────────────────┘   │
└─────────────────────────────────────────────┘
                  ▲
                  │ implements
┌─────────────────┴───────────────────────────┐
│         Adapters (infrastructure)           │
│  DieselWorkflowRepository (cheng-storage)   │
│  WorkflowEngine           (cheng-engine)    │
│  EventBusAdapter / WsManager (cheng-api)    │
└─────────────────────────────────────────────┘
```

| Crate | Layer | Owns |
|---|---|---|
| `cheng-core` | Domain | Models, ports, domain services, domain events. **No infrastructure.** |
| `cheng-storage` | Infrastructure | Diesel repositories, migrations, domain ↔ row conversion |
| `cheng-engine` | Application | `WorkflowEngine`, DAG analysis, scheduling, lifecycle |
| `cheng-api` | Presentation | Axum REST, WebSocket, dependency injection in `main.rs` |

Dependency injection happens exactly once, in `main.rs`. **There is no global state**, which is
why the engine and every service are testable with in-memory fakes and why the same
`Executor` can be handed to a REST handler, the CLI, the scheduler, and a
[subflow node](/docs/editor-subflows).

## Ports

Ports are traits in `cheng-core/src/ports/`. The list is long — over forty — and reading their
names is the fastest map of the system: `WorkflowRepository`, `ExecutionRepository`,
`ConversationRepository`, `DocumentRepository`, `DocumentShadowRepository`, `TableRepository`,
`KnowledgeBaseRepository`, `ArtifactStore`, `ExecutionArchiveStore`, `TraceSnapshotStore`,
`ScheduledTaskRepository`, `CredentialRepository`, `ChannelRepository`, `ChannelAdapter`,
`AdapterRegistry`, `SkillRepository`, `McpServerRepository`, `ApprovalEventBus`,
`ShadowEventBus`, `EventPublisher`, `Executor`, `NodeRegistry`, `WorkflowFileImporter`,
`WorkspaceToolBridge`, and more.

The `Executor` port is the one to understand first. `execute(workflow, options)` starts a run
and **returns immediately** with an `ExecutionId`; the run proceeds in the background.
`execute_by_id` loads from the repository first, which is what the frontend uses once a workflow
is saved.

## Execution pipeline

```text
1. REST receives an execution request
2. WorkflowEngine.execute() with the workflow id
3. Load from WorkflowRepository
4. Build a DAG from nodes and edges (petgraph)
5. Create the Execution record
6. Background task:
     topological sort → ready nodes
     TaskScheduler spawns async tasks
     node.execute(ExecutionContext)
     results persisted
     EventPublisher → WebSocket
     repeat until done or failed
7. Final state update
8. WebSocket broadcasts completion
```

The engine splits cleanly in two: the **static** half (DAG planner, validation, topological
sort, stage selection) is pure and stateless; the **dynamic** half (task scheduler) runs at
runtime. Because the plan is derived rather than stored, changing a workflow can never leave a
stale plan behind. [Execution Model](/docs/exec-model) covers this in depth.

## Key domain types

- **Strongly-typed ids**: `WorkflowId`, `NodeId`, `ExecutionId` — UUID newtypes, so you cannot
  pass an execution id where a workflow id belongs.
- **`NodeValue`**: a tagged union of String, Number, Boolean, Array, Object, File, Stream. The
  `File` and `Stream` cases are what let large payloads move by reference and let a model node
  emit tokens downstream as they arrive.
- **`ExecutionState`**: `pending`, `running`, `paused`, `waiting_for_review`, `completed`,
  `failed`, `cancelled`, `timeout` — **eight** states, the last four terminal.
- **Component identifiers**: node types are `category/name`, e.g. `io/input_text`, `ai/llm`.

## The node system

Nodes are registered by macro at compile time. `#[derive(Node)]` supplies metadata;
`register_node!` / `register_async_node!` add the type to an inventory the registry collects at
startup. There is no registration list to maintain — adding a file and rebuilding is enough.

Three implementation shapes:

| Trait | Use for |
|---|---|
| `TypedNode` | Synchronous, type-safe pure compute |
| `AsyncTypedNode` | Async, I/O-bound or context-dependent |
| `AsyncNode` | Raw `HashMap<String, Value>` — legacy |

The engine fetches the definition from `NodeRegistry`, validates the inputs against the node's
declared spec, calls `execute` with an `ExecutionContext`, and stores the outputs for downstream
nodes.

## Schema-driven UI

The backend emits JSON Schema with `x-*` UI extensions for every node; the frontend's **Field
Control Registry** turns that into form controls by priority matching. The consequence for
development is direct: **changing a node's inputs or outputs on the backend surfaces
automatically in the UI**, with no frontend change.

The priority chain is ordered — connected port status (100), file paths (90), sensitive field
names (85), component identifiers such as `model_AC` (65), enums (45), basic types (40),
fallback (0). Understanding it explains why a field renders as it does. See
[Node Configuration](/docs/editor-node-configuration).

Supported extensions include `x-control`, `x-provider-options`, `x-model-options`,
`x-depends-on`, `x-allow-custom`, `x-hidden`, and `x-port`.

## Cross-cutting patterns

1. **Event-driven** — `EventBusAdapter` bridges engine events to WebSocket.
2. **Async everywhere** — Tokio; all I/O is `async`.
3. **Streaming** — nodes can yield incremental results through the stream publisher.
4. **Errors** — `anyhow::Result` for application errors, `thiserror` for domain errors.
5. **Credentials** — `EncryptionService` plus `CredentialRepository`; an OAuth2 background
   service refreshes tokens.
6. **Channel auth** — `chk_`-prefixed API keys stored as credentials with
   `provider = "channel_adapter"`, checked by middleware.
7. **Ambient context** — `workspace_id`, `conversation_id`, `document_id` injected by the
   engine, force-overriding model-supplied values in tool calls.

## The frontend architecture

`chengflow-ui` is React 18 + TypeScript + Vite, with TanStack Router (file-based routes),
Zustand + Immer for client state, React Query for server state, React Flow for the canvas, and
Tailwind + Radix + shadcn/ui for presentation.

State is split across purpose-built stores rather than one global blob: `editorStore` (node and
edge CRUD, viewport, clipboard, validation), `workflowTabStore` (tabs and backend sync),
`historyStore` (undo/redo), `executionStore` (WebSocket execution state), `workspaceStore`
(active workspace), `shadowStore` (reviewable AI edits).

The app is **workspace-scoped**. Routes come in two shapes — legacy global (`/chat`,
`/editor/$id`) and workspace-scoped (`/w/$workspaceId/...`). `workspaceStore` is authoritative
for `activeWorkspaceId`, and feature modules read it rather than touching `localStorage`.
Resolution order on boot: URL → `localStorage` cache → backend `/me/preferences` → first
accessible workspace. The store **never auto-creates** a workspace.

Separately from the editor, `src/runtime/` renders PageLayout JSON — the output of workflows —
into live pages: a miniature app-rendering engine, with `scripts/generate-pages.ts` as its
build-time counterpart.

## Cloud service invariants

`chenghub` and `skill-registry` live outside this tree and hold to their own rules: PostgreSQL
is the only source of business truth and Redis is an accelerator that both services degrade
gracefully without; each has a single binary whose `migrate` subcommand is the only
schema-mutating path; the skill registry never receives ChengFlow credentials, never executes
skills, and content-addresses artifacts by SHA-256 with published artifacts immutable.

## Next steps

- [Crate Structure](/docs/dev-crate-structure) — every crate and its job.
- [Writing a Custom Node](/docs/dev-custom-node) — applying all of this.
- [Execution Model](/docs/exec-model) — the engine in detail.
