Read OSS

Storybook's Architecture: A Three-World System Connected by Channels

Intermediate

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 the internal prefix 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:

  1. Core commands (dev, build, index) are loaded directly from the compiled core binary via dynamic import().
  2. init is routed to the create-storybook package — either from a local install if versions match, or via npx.
  3. Everything else (upgrade, doctor, automigrate) goes to @storybook/cli, again with a version-matching fallback to npx.

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.