# How the n8n Vue.js Frontend Is Architected with Pinia Stores: A Deep Dive into State Management

> Explore the n8n Vue.js frontend architecture and learn how Pinia stores manage state. Discover the modular approach with RootStore, core UI stores, and feature-specific modules for efficient state management.

- Repository: [n8n - Workflow Automation/n8n](https://github.com/n8n-io/n8n)
- Tags: architecture
- Published: 2026-02-24

---

**The n8n editor UI implements a modular Pinia architecture using a single instance mounted on a Vue 3 application, splitting state into a central RootStore for configuration, core UI stores for global concerns, and feature-specific stores colocated with domain logic.**

The n8n workflow automation platform powers its visual editor through a **Vue.js frontend architected with Pinia stores** to manage complex reactive state across the application. This design separates environment configuration, interface state, and workflow data into scalable, modular stores. Understanding how n8n structures its Pinia implementation provides a blueprint for building maintainable enterprise-grade Vue 3 applications with predictable state management.

## Core Pinia Setup in the Vue 3 Application

The Pinia initialization happens once in the application entry point. In [`packages/frontend/editor-ui/src/main.ts`](https://github.com/n8n-io/n8n/blob/main/packages/frontend/editor-ui/src/main.ts), the code creates a single Pinia instance and installs it on the Vue app alongside legacy compatibility shims.

```typescript
// packages/frontend/editor-ui/src/main.ts
import { createApp } from 'vue';
import { createPinia, PiniaVuePlugin } from 'pinia';
import App from '@/app/App.vue';
import router from '@/app/router';

const pinia = createPinia();               // ← single Pinia instance
const app = createApp(App);

app.use(SentryPlugin);
app.use(PiniaVuePlugin);                  // Vue 2 compatibility shim for legacy plugins
app.use(pinia);                           // ← install Pinia on the app
app.use(router);
app.mount('#app');

```

The `PiniaVuePlugin` remains in the codebase to support legacy plugins during the transition from Vue 2, while `createPinia()` initializes the modern store instance. Once mounted via `app.use(pinia)`, every component in the tree can access stores through composable functions.

## The Root Store – Global Configuration

Centralized environment configuration lives in the **RootStore**, defined in `packages/frontend/@n8n/stores/src/useRootStore.ts`. This store manages API endpoints, localization settings, and session identifiers derived from meta tags or environment variables.

```typescript
// packages/frontend/@n8n/stores/src/useRootStore.ts
import { defineStore } from 'pinia';
import { STORES } from './constants';
import { getConfigFromMetaTag } from './metaTagConfig';

export const useRootStore = defineStore(STORES.ROOT, () => {
  const state = ref<RootStoreState>({
    baseUrl: import.meta.env.VUE_APP_URL_BASE_API ?? window.BASE_PATH,
    restEndpoint: getConfigFromMetaTag('rest-endpoint') ?? 'rest',
    pushRef: (() => {
      const key = 'n8n-client-id';
      const existing = sessionStorage.getItem(key);
      if (existing) return existing;
      const id = randomString(10).toLowerCase();
      sessionStorage.setItem(key, id);
      return id;
    })(),
    // … other config fields
  });

  // Computed getters
  const baseUrl = computed(() => state.value.baseUrl);
  const restUrl = computed(() => `${state.value.baseUrl}${state.value.restEndpoint}`);

  // Actions
  const setTimezone = (tz: string) => {
    state.value.timezone = tz;
    setGlobalState({ defaultTimezone: tz });
  };

  return {
    baseUrl,
    restUrl,
    setTimezone,
    // …
  };
});

```

This pattern demonstrates the **Setup Store** syntax introduced in Pinia 2.0: state is held in `ref` objects, getters use `computed`, and actions are plain functions. The store exports a composable (`useRootStore`) that components import to access these reactive properties.

## UI-Centric Stores for Global State

Beyond configuration, n8n organizes global UI state into domain-specific stores located under `packages/frontend/editor-ui/src/app/stores/`. Each store handles a distinct concern using the same `defineStore` pattern.

| Store | Responsibility | Key File |
|-------|----------------|----------|
| **ui.store** | Global UI flags (sidebar visibility, loading spinners, snackbars) | [`packages/frontend/editor-ui/src/app/stores/ui.store.ts`](https://github.com/n8n-io/n8n/blob/main/packages/frontend/editor-ui/src/app/stores/ui.store.ts) |
| **workflows.store** | Workflow list pagination, caching, and sidebar state | [`packages/frontend/editor-ui/src/app/stores/workflows.store.ts`](https://github.com/n8n-io/n8n/blob/main/packages/frontend/editor-ui/src/app/stores/workflows.store.ts) |
| **workflowState.store** | Active workflow editing state (nodes, connections, dirty flags) | [`packages/frontend/editor-ui/src/app/stores/workflowState.store.ts`](https://github.com/n8n-io/n8n/blob/main/packages/frontend/editor-ui/src/app/stores/workflowState.store.ts) |
| **logs.store** | Execution logs for active workflow runs | [`packages/frontend/editor-ui/src/app/stores/logs.store.ts`](https://github.com/n8n-io/n8n/blob/main/packages/frontend/editor-ui/src/app/stores/logs.store.ts) |
| **settings.store** | User preferences, dark mode, and default credentials | [`packages/frontend/editor-ui/src/app/stores/settings.store.ts`](https://github.com/n8n-io/n8n/blob/main/packages/frontend/editor-ui/src/app/stores/settings.store.ts) |
| **cloudPlan.store** | Cloud subscription metadata and limits | [`packages/frontend/editor-ui/src/app/stores/cloudPlan.store.ts`](https://github.com/n8n-io/n8n/blob/main/packages/frontend/editor-ui/src/app/stores/cloudPlan.store.ts) |
| **pushConnection.store** | WebSocket/SSE connection handling for real-time updates | [`packages/frontend/editor-ui/src/app/stores/pushConnection.store.ts`](https://github.com/n8n-io/n8n/blob/main/packages/frontend/editor-ui/src/app/stores/pushConnection.store.ts) |

### Managing Interface State in the UI Store

The UI store demonstrates how n8n handles transient interface state. It manages side-panel visibility, global loading indicators, and ephemeral notifications.

```typescript
// packages/frontend/editor-ui/src/app/stores/ui.store.ts
import { defineStore } from 'pinia';
import { STORES } from './constants';

export const useUiStore = defineStore(STORES.UI, () => {
  const isSidebarOpen = ref(true);
  const isLoading = ref(false);
  const snackbar = ref<{ message: string; type: 'success' | 'error' } | null>(null);

  const toggleSidebar = () => { 
    isSidebarOpen.value = !isSidebarOpen.value 
  };
  
  const showSnackbar = (msg: string, type: 'success' | 'error' = 'success') => {
    snackbar.value = { message: msg, type };
    setTimeout(() => (snackbar.value = null), 3000);
  };

  return {
    isSidebarOpen,
    isLoading,
    snackbar,
    toggleSidebar,
    showSnackbar,
  };
});

```

Components consume these stores directly without prop drilling, accessing `isSidebarOpen` or calling `showSnackbar()` through the imported composable.

## Feature-Specific Store Architecture

For domain logic isolated to specific functionality, n8n colocates Pinia stores within feature folders under `packages/frontend/editor-ui/src/features/`. This keeps the global store surface minimal while maintaining the benefits of centralized state management.

- [`features/workflows/history/workflowHistory.store.ts`](https://github.com/n8n-io/n8n/blob/main/features/workflows/history/workflowHistory.store.ts) – Recently opened workflow tracking
- [`features/workflows/readyToRun/stores/readyToRun.store.ts`](https://github.com/n8n-io/n8n/blob/main/features/workflows/readyToRun/stores/readyToRun.store.ts) – "Ready-to-run" banner state
- [`experiments/templateRecoV2/stores/templateRecoV2.store.ts`](https://github.com/n8n-io/n8n/blob/main/experiments/templateRecoV2/stores/templateRecoV2.store.ts) – Template recommendation data

### Isolating Domain Logic in Feature Stores

Feature stores can compose with global stores. The ready-to-run store imports `useRootStore` to access base configuration while maintaining its own isolated state.

```typescript
// packages/frontend/editor-ui/src/features/workflows/readyToRun/stores/readyToRun.store.ts
import { defineStore } from 'pinia';
import { STORES } from '../../../../app/stores/constants';
import { useRootStore } from '@n8n/stores';

export const useReadyToRunStore = defineStore(STORES.READY_TO_RUN, () => {
  const isVisible = ref(false);
  const workflowId = ref<string | null>(null);
  const root = useRootStore();  // Composition with global store

  const show = (id: string) => {
    workflowId.value = id;
    isVisible.value = true;
  };
  
  const hide = () => {
    workflowId.value = null;
    isVisible.value = false;
  };

  return { isVisible, workflowId, show, hide };
});

```

This architecture allows features to remain encapsulated while still accessing global configuration when necessary.

## Consuming Stores in Vue Components

Components access state by importing the store composable and invoking it within `<script setup>`. Pinia handles reactive subscriptions automatically.

```vue
<script setup lang="ts">
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { onMounted } from 'vue';

const workflowsStore = useWorkflowsStore();

onMounted(() => {
  workflowsStore.fetchWorkflows();   // Action defined in the store
});
</script>

<template>
  <div v-if="workflowsStore.isLoading">Loading…</div>
  <ul v-else>
    <li v-for="wf in workflowsStore.workflows" :key="wf.id">
      {{ wf.name }}
    </li>
  </ul>
</template>

```

The store instance (`workflowsStore`) exposes both reactive state (`workflows`, `isLoading`) and methods (`fetchWorkflows`) directly on the object.

## Store Registration and Naming Conventions

All stores reference centralized identifiers defined in [`packages/frontend/editor-ui/src/app/stores/constants.ts`](https://github.com/n8n-io/n8n/blob/main/packages/frontend/editor-ui/src/app/stores/constants.ts). This prevents naming collisions and enables TypeScript autocompletion.

```typescript
// packages/frontend/editor-ui/src/app/stores/constants.ts
export const STORES = {
  ROOT: 'root',
  UI: 'ui',
  WORKFLOWS: 'workflows',
  WORKFLOW_STATE: 'workflowState',
  LOGS: 'logs',
  SETTINGS: 'settings',
  READY_TO_RUN: 'readyToRun',
  // …
} as const;

```

Pinia registers stores **lazily**—the store initializes only when its composable is first called in a component. This prevents unnecessary overhead for features not currently in use.

## Summary

- **Single Pinia instance** created in [`main.ts`](https://github.com/n8n-io/n8n/blob/main/main.ts) and mounted on the Vue 3 application with `app.use(pinia)`.
- **RootStore** centralizes environment configuration, API endpoints, and session identifiers in `packages/frontend/@n8n/stores/src/useRootStore.ts`.
- **Core UI stores** (`ui.store`, `workflows.store`, `workflowState.store`, etc.) live under `app/stores` and handle global application state.
- **Feature stores** are colocated with domain logic under `features/.../stores/` for better code isolation and maintainability.
- **Setup Store syntax** using `defineStore` with `ref`, `computed`, and functions provides type-safe, composable state management.
- **Lazy loading** ensures stores initialize only when components consume them, optimizing application startup performance.

## Frequently Asked Questions

### Why does n8n use Pinia instead of Vuex for state management?

Pinia provides native TypeScript support, a lighter footprint, and a composition API-compatible syntax that aligns with Vue 3 best practices. The **Setup Store** pattern used throughout n8n (with `ref` and `computed`) mirrors the `<script setup>` component syntax, reducing cognitive overhead for developers moving between components and stores.

### How does n8n handle store-to-store communication?

Stores communicate through direct imports of other store composables. For example, feature stores like `useReadyToRunStore` import `useRootStore` from `@n8n/stores` to access global configuration. This follows Pinia's recommended pattern for cross-store dependencies while maintaining reactivity across module boundaries.

### What is the purpose of the PiniaVuePlugin in a Vue 3 application?

The `PiniaVuePlugin` provides compatibility shims for legacy plugins and code that may still rely on Vue 2 patterns during n8n's migration period. While the core application uses Vue 3's `createPinia()`, this plugin ensures older dependencies continue to function correctly in the hybrid environment.

### Where are feature-specific Pinia stores located in the n8n codebase?

Feature-specific stores reside within their respective domain folders under `packages/frontend/editor-ui/src/features/`. For example, the ready-to-run functionality stores its state in [`features/workflows/readyToRun/stores/readyToRun.store.ts`](https://github.com/n8n-io/n8n/blob/main/features/workflows/readyToRun/stores/readyToRun.store.ts), while workflow history uses [`features/workflows/history/workflowHistory.store.ts`](https://github.com/n8n-io/n8n/blob/main/features/workflows/history/workflowHistory.store.ts). This colocation keeps state logic adjacent to the components and composables that consume it.