Storybook's Architecture: A Three-World System Connected by Channels
Prerequisites
- ›General web development knowledge (HTML, CSS, JavaScript)
- ›Basic understanding of Node.js and npm/yarn
- ›Familiarity with monorepo concepts
- ›Understanding of iframes and cross-frame communication
Storybook's Architecture: A Three-World System Connected by Channels
Storybook is the most widely adopted tool for building UI components in isolation. But for something that appears to be a simple sidebar-plus-preview window, its internals are surprisingly deep. At runtime, Storybook operates as three separate environments — a Node.js server, a browser-based Manager UI, and a sandboxed Preview iframe — all communicating through an event-driven channel abstraction. What was once a sprawl of dozens of @storybook/* packages has been consolidated into a single storybook package with around 40 subpath exports and a carefully classified build tool.
This article maps out that architecture end-to-end. We'll trace the complete flow from typing storybook dev in your terminal through preset resolution, server startup, and browser launch. By the end, you'll have a mental model for how every piece of Storybook connects.
The Unified Core Package
Until recently, using Storybook meant installing a constellation of packages: @storybook/core-server, @storybook/channels, @storybook/preview-api, @storybook/manager-api, and many more. The Storybook team consolidated all of these into a single storybook package, published from code/core.
The package.json exposes the entire public API surface through subpath exports:
code/core/package.json#L48-L220
Each export follows a consistent pattern: a types condition pointing to declaration files, a code condition pointing to source (for tooling), and a default condition pointing to the compiled output. Public APIs like storybook/preview-api and storybook/manager-api sit at the top level; internal modules live under storybook/internal/*.
| Export Path | Purpose | Environment |
|---|---|---|
storybook/preview-api |
Story rendering, decorators, args | Browser (iframe) |
storybook/manager-api |
Manager state, addon hooks | Browser (parent) |
storybook/test |
Testing utilities (expect, fn) |
Browser (iframe) |
storybook/internal/core-server |
Dev/build server orchestration | Node.js |
storybook/internal/channels |
Cross-environment communication | Both |
storybook/internal/core-events |
Event name constants | Both |
Tip: When reading Storybook source, imports from
storybook/internal/*are internal APIs that may change between minor versions. Public APIs without theinternalprefix are the stable contract.
Three Environments: Server, Manager, and Preview
Storybook's three-environment model isn't arbitrary — it emerges from a fundamental constraint: component rendering must be fully isolated from the development tooling.
flowchart TB
subgraph Node["Node.js Server"]
CS[Core Server]
SIG[StoryIndexGenerator]
PS[Preset System]
end
subgraph Browser["Browser Window"]
subgraph Manager["Manager (Parent Frame)"]
Sidebar[Sidebar]
Toolbar[Toolbar]
Addons[Addon Panels]
end
subgraph Preview["Preview (iframe)"]
Story[Rendered Story]
Decorators[Decorators]
PlayFn[Play Functions]
end
end
Node -->|"WebSocket (dev only)"| Manager
Node -->|"WebSocket (dev only)"| Preview
Manager <-->|"PostMessage"| Preview
The Server (Node.js) handles filesystem access, story indexing, static file serving, and the build pipeline. It runs Polka as an HTTP server and exposes a WebSocket endpoint for real-time communication.
The Manager is a full React application rendering the sidebar, toolbar, and addon panels. It lives in the parent browser frame and is built with esbuild.
The Preview lives in a sandboxed iframe. This is where your actual components render. It's built with Vite or webpack, depending on your framework choice. The iframe isolation ensures your component's styles, globals, and side effects can't leak into Storybook's UI — and vice versa.
The Monorepo Layout
The Storybook repository is organized as a monorepo under the code/ directory. Here's the key structure:
| Directory | Purpose | Examples |
|---|---|---|
code/core/ |
The unified storybook package — everything ships from here |
channels, preview-api, manager-api, core-server |
code/frameworks/ |
Framework-specific presets wiring a builder + renderer | react-vite, angular, svelte-vite |
code/builders/ |
Build tool integrations | builder-vite, builder-webpack5 |
code/renderers/ |
Framework-specific rendering implementations | react, vue3, svelte |
code/addons/ |
Official addon packages | docs, a11y, themes, vitest |
code/lib/ |
Standalone utility libraries | cli-storybook, create-storybook, codemod |
scripts/ |
Build, check, and sandbox orchestration | build-package.ts, task runners |
The key insight is that frameworks are thin preset wrappers. A framework like react-vite simply declares which builder and renderer to use:
code/frameworks/react-vite/src/preset.ts#L5-L8
export const core: PresetProperty<'core'> = {
builder: import.meta.resolve('@storybook/builder-vite'),
renderer: import.meta.resolve('@storybook/react/preset'),
};
Everything else — build configuration, dev server setup, story indexing — is handled by the core and composed through the preset system.
The CLI Dispatcher
When you run storybook dev, the entry point is the dispatcher at code/core/src/bin/dispatcher.ts. It's elegant in its simplicity: a small routing function that decides where to send each command.
code/core/src/bin/dispatcher.ts#L36-L87
flowchart LR
CLI["storybook <command>"] --> Check{Command?}
Check -->|"dev / build / index"| Core["core.js (internal)"]
Check -->|"init"| Create["create-storybook (npx)"]
Check -->|"upgrade / doctor / ..."| SBCli["@storybook/cli (npx)"]
Three routing paths exist:
- Core commands (
dev,build,index) are loaded directly from the compiled core binary via dynamicimport(). initis routed to thecreate-storybookpackage — either from a local install if versions match, or vianpx.- Everything else (
upgrade,doctor,automigrate) goes to@storybook/cli, again with a version-matching fallback tonpx.
Before any routing happens, the dispatcher validates the Node.js version — Storybook requires Node 20.19+ or 22.12+. This is a hard gate at line 24, not a soft warning.
Tracing storybook dev End to End
Let's trace the complete journey from storybook dev to a browser window opening with your stories.
sequenceDiagram
participant CLI as CLI Dispatcher
participant BDS as buildDevStandalone
participant Load as loadAllPresets
participant Server as Polka Server
participant MB as Manager Builder (esbuild)
participant PB as Preview Builder (Vite)
participant Browser as Browser
CLI->>BDS: import core.js → buildDevStandalone()
BDS->>BDS: Resolve port, configDir, outputDir
BDS->>Load: First pass (determine builder)
Load-->>BDS: Builder info + core config
BDS->>BDS: Create WebSocket channel
BDS->>Load: Second pass (all presets)
Load-->>BDS: Full options with presets
BDS->>Server: storybookDevServer(options, server)
Server->>MB: managerBuilder.start()
Server->>PB: previewBuilder.start()
Server->>Server: app.listen(port)
Server->>Browser: openInBrowser(address)
The journey begins in buildDevStandalone():
code/core/src/core-server/build-dev.ts#L44-L51
This function orchestrates the entire dev experience. It resolves the port (prompting if the desired port is occupied), loads the main config, validates the framework, and then performs the critical two-pass preset loading — first to determine the builder, then to load all presets with the builder's override presets included.
The actual HTTP server is Polka, a minimal alternative to Express:
code/core/src/core-server/dev-server.ts#L28-L34
The dev server sets up middleware (compression, host validation, access control, caching), registers the index.json route that serves the story index, and then kicks off both builders in parallel. The manager builder uses esbuild; the preview builder uses Vite or webpack depending on your framework.
The Node/Browser/Runtime Build Split
The build-config.ts file in the core package classifies every entry point into one of four categories:
code/core/build-config.ts#L22-L210
flowchart TD
BC[build-config.ts] --> Node[node entries]
BC --> Browser[browser entries]
BC --> Runtime[runtime entries]
BC --> GR[globalizedRuntime entries]
Node --> N1[core-server]
Node --> N2[node-logger]
Node --> N3[telemetry]
Node --> N4[csf-tools]
Node --> N5[common utilities]
Browser --> B1[preview-api]
Browser --> B2[manager-api]
Browser --> B3[channels]
Browser --> B4[theming]
Browser --> B5[components]
Runtime --> R1[preview/runtime]
Runtime --> R2[manager/globals-runtime]
Runtime --> R3[mocker-runtime]
GR --> G1[manager/runtime.tsx]
Node entries are server-side code that can use fs, path, and other Node APIs. They're bundled with Node as the target platform.
Browser entries are client-side code that runs in the browser. They're bundled with browser compatibility targets (Chrome 100+, Safari 15+, Firefox 91+).
Runtime entries are special — they're browser code that must be bundled without code splitting because they're injected as standalone scripts into the preview iframe.
Globalized runtime entries (just the manager runtime) are wrapped to expose their exports as globals on the window object, enabling addon interoperability.
This classification isn't just a build optimization. It's a safety boundary: importing a node entry from browser code (or vice versa) would break at runtime. The build tool enforces the separation by building each category with different esbuild configurations.
Tip: If you're contributing to Storybook and adding a new module, deciding which category it belongs to is one of the first architectural decisions you'll make. Ask: "Does this code need the filesystem? Does it run in an iframe? Does it need to be available as a global?"
What's Next
We've established the high-level architecture: three environments, a unified package with classified build entries, and a CLI that dispatches to a two-pass preset loading system. But we've only scratched the surface of how configuration actually works. In the next article, we'll dive deep into the preset system — Storybook's configuration kernel that composes settings from dozens of sources through a functional reduce chain.