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 { avatarSrc } from '@stores/store.svelte';
// This adds a manual sync step, a store file, 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
Core Application
| Store | Type | Purpose |
|---|---|---|
store.svelte.ts |
App singleton + re-exports | Minimal AppStore — listboxValueState, tabSetState, shouldShowNextButton, saveLayerStore. Re-exports from domain stores. No wrapper objects. |
locale-store.svelte.ts |
Reactive wrappers | systemLanguage, contentLanguage, translationProgress — bridges server cookies → Paraglide runtime |
user-store.svelte.ts |
Utility | normalizeAvatarUrl() — pure function, not a store. Avatar URL comes from page.data.user?.avatar. |
validation-store.svelte.ts |
Singleton class | Per-field form validation errors, isValid derived |
data-change-store.svelte.ts |
Singleton class | JSON snapshot comparison for unsaved changes detection |
| Store | Type | Purpose |
|---|---|---|
global-settings.svelte.ts |
Singleton + SSE | Public environment settings (SITE_NAME, PKG_VERSION). Proxy-based publicEnv for reactive access. |
screen-size-store.svelte.ts |
Singleton class | Viewport dimensions (width, height), breakpoint queries (isMobile, isTablet, isDesktop) |
ui-store.svelte.ts |
Singleton class | Panel visibility (leftSidebar, rightSidebar, pageheader, etc.). Route context + screen-size driven. |
theme-store.svelte.ts |
Module-level $state |
Dark/light/system color scheme. Cookie persistence. OS preference listener. |
Feature Stores
| Store | Type | Purpose |
|---|---|---|
collection-store.svelte.ts |
Singleton class | Active collection context — schema, mode (view/edit/create), entry value, content structure |
content-registry.svelte.ts |
Singleton class | Compiled content tree — ContentNode hierarchy and derived Schema catalog. Multi-tenant. |
mode-transition-guard.svelte.ts |
State machine | Validates mode transitions (blocks edit→view with unsaved changes). Consolidated setMode() + transitionTo(). |
status-store.svelte.ts |
Service + $state |
Publish/unpublish toggle with API calls, debounce, and toast notifications |
widget-store.svelte.ts |
Singleton class | Widget registry — core/custom/marketplace widgets, dependency analysis, active set management |
| Store | Type | Purpose |
|---|---|---|
image-editor-store.svelte.ts |
Factory function | Canvas state — zoom, rotation, filters, crop, history stack, compare slider |
collaboration-store.svelte.ts |
Singleton class | Real-time WebSocket — activity stream, chat messages, room management via svelte-realtime |
loading-store.svelte.ts |
Singleton class | Global loading state with priority queue, timeouts, progress tracking, and analytics |
toast.svelte.ts |
Singleton class | Enterprise toast notification system with responsive positioning, pause-on-hover, flash messages |
setup-store.svelte.ts |
Factory function | Setup wizard form state with localStorage persistence, validation, and step management |
User Preferences & Personalization
| Store | Type | Purpose |
|---|---|---|
user-prefs-overlay.svelte.ts |
Optimistic overlay | Per-user density/variant/accessibility overrides. Apply immediately, sync to server async. |
dashboard-preferences.svelte.ts |
Server-persisted | Dashboard widget layout preferences (/api/system-preferences) |
pinned-store.svelte.ts |
localStorage + $state |
User-pinned collections and media folders in sidebar |
consent-store.svelte.ts |
localStorage + $state |
GDPR cookie consent preferences (analytics, marketing) |
UI Micro-State
| Store | Type | Purpose |
|---|---|---|
active-input-store.svelte.ts |
Singleton class | Currently focused token input (for TokenPicker insertion) |
settings-config-state.svelte.ts |
SvelteSet |
Tracks which settings groups need configuration (empty required fields) |
plugin-workspace.svelte.ts |
URL-synced $state |
Active plugin workspace overlay — ?plugin=<id> search param |
System Health (system/)
| File | Type | Purpose |
|---|---|---|
state.svelte.ts |
Reactive container | System state machine (IDLE → READY → DEGRADED), service health tracking |
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-store, screen-size-store, collection-store, collaboration-store, loading-store
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, toast
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: pinned-store, consent-store
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: user-prefs-overlay
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 store
function syncUserToStore(user: User) {
app.avatarSrc = user.avatar; // ← unnecessary
}
// GOOD: Components read page.data directly
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, data-change-store, user-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 "@stores/dashboard-preferences.svelte";
// GOOD: DB adapters stay in the data layer
5. File Count Summary
| Category | Count |
|---|---|
| Core application | 9 |
| Feature stores | 8 |
| User preferences | 4 |
| UI micro-state | 3 |
System health (system/) |
6 |
| Total | 30 |