Store System Architecture
Complete reference for SveltyCMS client-side stores — patterns, purpose, and when to use page.data instead.
On this page
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 |