Octonode Playbook

Studio component architecture (apps/studio) — GOAL MODE

Canonical Octonode repository documentation.

Studio component architecture (apps/studio) — GOAL MODE

When you touch apps/studio, these are goals to hit, not suggestions. Every component you create or edit must end in this shape. If an existing file violates it, bring it into compliance as part of your change.

Atomic design, one component per folder. Every component lives in its own folder named after it (PascalCase). The folder holds only what that component needs:

ComponentName/
  ComponentName.tsx          # the component — JSX + wiring only
  ComponentName.types.ts     # props + local types
  ComponentName.module.css   # styling (only if it needs non-Tailwind styles)
  constants.ts               # local constants (only if needed)
  helpers.ts                 # pure utils (only if needed)
  index.ts                   # barrel — see "Every folder has a barrel" below

.module.css / constants.ts / helpers.ts are created only when needed — don't scaffold empty files.

Every folder has a barrel index.ts that re-exports the component's whole surface. Not just the component — its types, constants, helpers, and nested atoms too, so a consumer can reach any of a component's infra from the folder route (import { X } from ".../ComponentName"):

// ComponentName/index.ts
export * from "./ComponentName";
export * from "./ComponentName.types";
export * from "./constants";
export * from "./helpers";
export * from "./ChildAtom";   // nested atom folders, too

Import from the folder, never deep-reach into its files from outside.

Component-under-component → folder-under-folder. A subcomponent that belongs to one parent is nested inside it, with the same internal structure:

WorkflowsListView/
  WorkflowsListView.tsx
  WorkflowRow/               # the list's atom — its own folder
    WorkflowRow.tsx
    WorkflowRow.types.ts
    index.ts
  FolderGroup/
    ...

A list renders its atom, not the row markup. A list/table component composes the atom component (<WorkflowRow .../>), it does not inline the full row JSX. Same for menus, panels, cards, accordions — extract the unit and render it.

Max ~200–250 lines per component file. If a .tsx grows past that, the fix is always one of: extract an atom into its own folder, or move logic into a hook. Never let a page file hold both the orchestration and the markup of its children.

All hooks live in src/hooks/, grouped into scope folders, and data hooks use React Query. No useQuery/useMutation inside a component file, and no custom hooks defined inline. Every hook — React Query data hooks and component-orchestration hooks (state reducers, controllers, derived-data hooks) — lives under apps/studio/src/hooks/, in a folder per scope so it is obvious which hook belongs to which domain. Each scope folder has its own barrel; the root hooks/index.ts re-exports them all and hooks/queryKeys.ts holds the shared keys:

hooks/
  index.ts            # root barrel — re-exports every scope
  queryKeys.ts        # shared React Query keys (cross-scope)
  workflows/          # useWorkflows*  (list / create / delete / meta)
  graph/              # useGraph*      (graph query + node/topology mutations)
  nodes/              # useNodes*      (project / native / scoped catalogs)
  marketplace/        # useMarketplace*
  identity/           # useIdentity
  app/                # useStudioController, useUiState, useRunState (top-level orchestration)
  canvas/             # useCanvasGraph (xyflow state)
  workflows-list/     # useWorkflowFilters
  executions/         # useExecutionSelection

Components import hooks from ../../hooks (or a scope folder, e.g. ../../hooks/workflows, to pull just that domain); the component body just calls hooks and renders.

The litmus test for a component file: imports → call hooks → derive trivial view values → return JSX composed of atoms. Anything heavier than that belongs in a hook or an atom.

Data hooks reach the backend only through the focused modules under apps/studio/src/api/ — see api-codegen.md.

The Types, Values, and Classes & services workspaces are source-backed views, not a second database. Their read/write ownership, class blueprint, decorator modes, memory lifetimes, and revision-safe synchronization are defined in Studio TypeScript classes and services.

On this page