Skip to content

Documentation

Store System Architecture

Complete reference for SveltyCMS client-side stores — patterns, purpose, and when to use page.data instead.

9/22/2026
8 min read Edit on GitHub

SveltyCMS uses Svelte 5 runes ($state, $derived, $effect) for all client-side state. Stores are organized by domain — each file owns one concept. Server data flows through SvelteKit’s page.data (from +layout.server.ts), not through global stores.


1. Core Principle: page.data over Stores

SvelteKit injects server data into every component via page.data. Do not duplicate this into a store.

<!-- ✅ CORRECT: Derive from page data -->
<script>
  let { data } = $props();
  const avatarUrl = $derived(data.user?.avatar ?? '/Default_User.svg');
</script>

<!-- ❌ WRONG: Sync page data into a store, then read the store -->
<script>
  import { collections } from '@src/stores/collection-store.svelte';
  // Mirroring server data into a singleton adds a sync step and 0 value
</script>

Use a store only when:

  • The value changes without page navigation (user toggle, WebSocket event, timer)
  • Multiple unrelated components need to react to the same change
  • The value is genuinely client-only (viewport size, canvas state, form draft)

Use page.data when:

  • The value comes from the server (user data, settings, collections)
  • Changes always trigger SSR navigation (language switch, route change)
  • Only descendant components of a layout need the value

2. Store Directory

The directory holds 18 domain modules plus the system/ health state machine — one concept per file. Every store is a Svelte 5 rune module ($state / $derived), never a svelte/store writable.

Shell & Environment

Store Export Purpose
global-settings.svelte.ts publicEnv Public environment settings (SITE_NAME, PKG_VERSION, …) — proxy-based reactive reads; initPublicEnv() hydrates the store and the values are typed as InferOutput<typeof publicConfigSchema> (validated server-side before they reach it). Private settings are never imported here.
ui-store.svelte.ts ui Shell visibility (leftSidebar, rightSidebar, pageheader, pagefooter, chatPanel, …), route context, global search visibility, sticky actions, listboxValueState, and the editor-wizard transient state (ui.wizard: shouldShowNextButton, saveLayerStore, tabSetState, headerActionButton).
screen-size-store.svelte.ts screen, ScreenSize Viewport width/height with SSR-safe defaults, BREAKPOINTS, getScreenSize() helper
theme-store.svelte.ts themeStore Theme + dark-mode preference persistence, initializeThemeStore(), toggleDarkMode(), and the optimistic userThemePrefs overlay
locale-store.svelte.ts locale systemLanguage / contentLanguage plus translationProgress.markFieldTranslated() for the translation cockpit
loading-store.svelte.ts globalLoadingStore Priorities, timeouts, progress and cancellation (loadingOperations, withLoading()); drives the navigation overlay
floating-nav-store.svelte.ts floatingNavStore Per-user FloatingNav preferences: SYSTEM_ENDPOINTS catalog, enabled system routes, arbitrary page favorites, per-user localStorage keying
plugin-workspace.svelte.ts pluginWorkspace Active plugin workspace overlay, URL-synced through ?plugin=<id>
active-input-store.svelte.ts activeInput The currently focused token input (element + metadata) set by fields.svelte — its consumer (token-picker.svelte) is currently unmounted
media-folder-tree.svelte.ts mediaFolderTree Shared flat list of media virtual folders for the sidebar tree and the mobile drag rail (one fetch, many consumers)
setup-store.svelte.ts setupStore Setup-wizard service store: form state, validation, DB test / seed / complete calls, localStorage persistence

Content & Editing

Store Export Purpose
collection-store.svelte.ts collections Active collection schema, mode (view/edit/create/media) and the reactive activeValue of the entry being edited; convenience setters setCollection, setMode, setCollectionValue, plus statusMap. Also hosts collectionMetadata (favorites/tags), pinnedStore, and getTagColor()
content-registry.svelte.ts contentStore Content-tree registry: per-tenant ContentNode hierarchy, derived Schema catalog, O(1) path/alias indexes, client-node cache and contentVersion for HMR-aware reloads
mode-transition-guard.svelte.ts modeTransitionGuard Single source of truth for mode changes: instant setMode() vs validated transitionTo() (blocks edit/create → view with unsaved changes)
validation-store.svelte.ts validationStore Per-field error messages with a derived isValid and clearAllErrors()
widget-store.svelte.ts widgets Widget registry (core/custom/marketplace), tenant-aware status, dependency analysis and active-set management (getWidget, isWidgetActive)

Colocated Stores (Single-Consumer)

Single-consumer stores live with their feature instead of in src/stores/:

Store Export Lives with
plugins/cookie-consent/consent-store.svelte.ts consentStore GDPR cookie-consent plugin
components/image-editor/image-editor-store.svelte.ts imageEditorStore Image editor (zoom, crop, filters, history)
components/system/help-state.svelte.ts helpStore Contextual help panel
routes/(app)/config/system-settings/settings-state.svelte.ts groupsNeedingConfig (SvelteSet) System Settings groups with empty required fields
routes/(app)/dashboard/dashboard-preferences.svelte.ts preferences (systemPreferences) Dashboard widget layout, hydrated from the layout load

System Health (system/)

File Type Purpose
state.svelte.ts Reactive container SystemStateContainer + global systemStateStore, service health tracking, setSystemState(), updateServiceHealth()
types.ts Types SystemState, ServiceHealth, ServicePerformanceMetrics, anomaly detection types
config.ts Constants Baseline times, anomaly thresholds, initial state objects
metrics.ts Functions Performance tracking, anomaly detection, self-calibration, metric persistence
reporting.ts Functions Health check reports, bottleneck identification, timeout recommendations
async.ts Async utilities waitForSystemReady(), waitForServiceHealthy() with AbortSignal and intelligent timeouts

3. Store Patterns

Pattern A: Singleton Class with $state

Used by: ui, collections, screen, validationStore, globalLoadingStore, toast, widgets

class MyStore {
  value = $state<string>("default");

  get computed(): boolean {
    return this.value !== "default";
  }

  update(v: string) {
    this.value = v;
  }
}

export const myStore = new MyStore();

When to use: Cross-component shared state that changes without page navigation.

Pattern B: Module-level $state

Used by: theme-store (module-level $state behind a getter object); the same getter-object shape is used for locale-store’s systemLanguage / contentLanguage / translationProgress exports.

const state = $state({ dark: true });

export const myStore = {
  get isDark() {
    return state.dark;
  },
  toggle() {
    state.dark = !state.dark;
  },
};

When to use: Simpler than a class, no inheritance needed, fewer than 5 methods.

Pattern C: localStorage + $state

Used by: pinnedStore, collectionMetadata, floatingNavStore, setupStore, and the colocated consentStore (plugin) / preferences (dashboard)

class MyStore {
  items = $state<string[]>([]);

  constructor() {
    if (browser) this.load();
  }

  private load() {
    const stored = localStorage.getItem("key");
    if (stored) this.items = JSON.parse(stored);
  }

  private save() {
    localStorage.setItem("key", JSON.stringify(this.items));
  }
}

When to use: User preference that persists across sessions, no server round-trip needed.

Pattern D: Optimistic Overlay

Used by: theme-store → userThemePrefs (absorbed from the removed user-prefs-overlay store)

class MyOverlay {
  #optimistic = $state<Prefs | null>(null);

  apply(prefs: Prefs) {
    this.#optimistic = { ...this.#optimistic, ...prefs };
  }
  release() {
    this.#optimistic = null;
  }
  getEffective(server?: Prefs) {
    return { ...server, ...this.#optimistic };
  }
}

When to use: Instant UI feedback before server confirms. Server becomes authoritative on next data fetch.

Pattern E: State Machine

Used by: mode-transition-guard

class MyStateMachine {
  private transitions = [{ from: "a", to: "b", validate: () => this.check() }];

  async transitionTo(state: State): Promise<boolean> {
    // Validate, run hooks, perform transition
  }
}

When to use: Complex state with guarded transitions (unsaved changes, permission checks).

Pattern F: URL-Synced $state

Used by: plugin-workspace

class MyUrlStore {
  value = $state<string | null>(readFromUrl());

  set(v: string) {
    this.value = v;
    history.pushState({}, "", `?param=${v}`);
  }
}

When to use: State that should survive page reloads and be shareable via URL.


4. Anti-Patterns to Avoid

❌ Duplicating page.data into a store

// BAD: Manual sync function copies server data into a store
function syncUserToStore(user: User) {
  ui.userAvatar = user.avatar; // ← unnecessary
}

// GOOD: Components read page.data directly
let { data } = $props();
const avatarUrl = $derived(data.user?.avatar ?? "/Default_User.svg");

❌ God Object stores

// BAD: One file exports 5 unrelated classes
// store.svelte.ts used to export AppStore, ValidationStore, DataChangeStore,
// translationProgress, avatarSrc, normalizeAvatarUrl, toast, tabSet...

// GOOD: Each domain has its own file
// locale-store, validation-store, collection-store, ui-store

❌ Stale snapshot exports

// BAD: Primitive export loses reactivity
export const isLoading = store.isLoading; // ← always false

// GOOD: Reactive getter preserves reactivity
export const isLoading = {
  get value() {
    return store.isLoading;
  },
};

❌ Server-only modules importing UI stores

// BAD: DB adapter imports dashboard preferences
import { preferences } from "../../routes/(app)/dashboard/dashboard-preferences.svelte";

// GOOD: DB adapters stay in the data layer

5. File Count Summary

Category Count
Shell & environment (src/stores/) 13
Content & editing (src/stores/) 6
Colocated single-consumer stores 5
System health (src/stores/system/) 6
Total 30

Related

architecturestoresstate-managementsvelte-5page-data
Was this page helpful?