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, tooImport 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/ # useExecutionSelectionComponents 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.