Skip to content

Documentation

Boot and Initialization

How SveltyCMS boots, compiles collections on restart, and moves from setup to READY.

9/22/2026
34 min read Edit on GitHub
On this page

This page is the engine side of first run: the installer’s phases, the config it generates, the state machine that follows, and how a restart recompiles collections without moving UUIDs. The operator’s procedure — prerequisites, the six steps, verification, and recovery — is the Setup wizard guide, and install commands are Getting started.


On this page

Install and first run

Install, the setup wizard, and the first collection are documented once:

The rest of this page is what happens after that install: how a restart compiles collections, how collection UUIDs stay stable, and how the state machine reaches READY.

Part II: System Architecture {#body-part-ii}

Visual Overview

Complete System Lifecycle

graph TB
    subgraph "First-Time Setup"
        A1[Fresh Install] -->|No private.ts| A2[Vite: Setup Mode]
        A2 --> A3[Create Blank Config]
        A3 --> A4[Open /setup Wizard]
        A4 --> A5[User: Enter DB Config]
        A5 --> A6[Action: testDatabase]
        A6 -->|✓ Valid| A7[User: Select Language]
        A7 --> A8[Action: seedDatabase]
        A8 --> A9[Write Credentials]
        A8 --> A10[Seed Settings/Themes]
        A8 --> A11[Create Collections]
        A12[User: Admin Form] --> A13[Action: completeSetup]
        A13 --> A14[Create Admin + Session]
        A13 --> A15[Init Global System]
        A13 --> A16[Content System Init]
        A16 --> A17[READY State]
    end

    subgraph "Server Restart (Phased)"
        B1[Server Start] -->|private.ts exists| B2[Vite: Normal Mode]
        B2 --> B3[Scan Compiled Collections]
        B3 --> B4{For Each Collection}
        B4 --> B5{Hash Changed?}
        B5 -->|No| B6[Skip - Preserve UUID]
        B5 -->|Yes| B7[Recompile - Keep UUID]
        B6 --> B8[First Request]
        B7 --> B8
        B8 --> B9[handleSystemState]
        B9 --> B10[Boot Phase: SETUP]
        B10 --> B11[Boot Phase: CORE (Auth/Content)]
        B11 --> B13[READY State]
        B13 --> B12[Boot Phase: FULL (Cache/Themes)]
        B12 --> B14[WARMED State]
    end

    A17 -.->|Restart Server| B1

    style A1 fill:#f0f0f0,stroke:#333,stroke-width:3px
    style A4 fill:#e0e0e0,stroke:#333,stroke-width:2px
    style A8 fill:#e0e0e0,stroke:#333,stroke-width:2px
    style A13 fill:#e0e0e0,stroke:#333,stroke-width:2px
    style A17 fill:#d0d0d0,stroke:#333,stroke-width:4px
    style B1 fill:#f0f0f0,stroke:#333,stroke-width:3px
    style B7 fill:#e0e0e0,stroke:#333,stroke-width:2px
    style B14 fill:#d0d0d0,stroke:#333,stroke-width:4px

First-Time Setup Architecture

graph TB
    A[Vite Startup] -->|No private.ts| B[setupWizardPlugin]
    B --> C[Create Blank private.ts]
    B --> D[Compile Collections]
    B --> E[Open /setup in Browser]

    E --> F[User: DB Config]
    F --> G[Action: testDatabase]
    G -->|Success| H[User: System Language]

    H --> I[Action: seedDatabase]
    I --> J[Write private.ts]
    I --> K[Seed Settings]
    I --> L[Seed Themes]
    I --> M[Seed Collections]

    N --> O[Action: completeSetup]
    O --> P[Create Admin User]
    O --> Q[Initialize Global System]
    O --> R[Initialize Content System]
    O --> S[Set Session Cookie]

    S --> T[Redirect to CMS]

    style A fill:#f0f0f0,stroke:#333,stroke-width:3px
    style E fill:#e0e0e0,stroke:#333,stroke-width:2px
    style I fill:#e0e0e0,stroke:#333,stroke-width:2px
    style O fill:#e0e0e0,stroke:#333,stroke-width:2px
    style T fill:#d0d0d0,stroke:#333,stroke-width:4px

Server Restart Architecture

graph TB
    A[Vite Startup] -->|private.ts exists| B[cmsWatcherPlugin]
    B --> C[Scan Compiled Collections]
    C --> D{For Each Collection}

    D --> E{Hash Changed?}
    E -->|No| F[Skip Compilation]
    E -->|Yes| G[Recompile]

    F --> H[Preserve UUID]
    G --> H

    H --> I[First HTTP Request]
    I --> J[handleSystemState]
    J --> K[Boot Phase: SETUP (Connection)]
    K --> L[Boot Phase: CORE (Auth/Settings/Content)]
    L --> N[READY State]
    N --> M[Boot Phase: FULL (Cache/Themes/Background)]
    M --> P[WARMED State]

    N --> Q[Content System Auto-Init]
    Q --> R[Register Collections]

    style A fill:#f0f0f0,stroke:#333,stroke-width:3px
    style B fill:#e0e0e0,stroke:#333,stroke-width:2px
    style G fill:#e0e0e0,stroke:#333,stroke-width:2px
    style H fill:#d0d0d0,stroke:#333,stroke-width:3px
    style N fill:#d0d0d0,stroke:#333,stroke-width:4px

First-Time Setup Flow

Phase 1: Vite Startup (Build Time) {#setup-phase1}

File: vite.config.ts

sequenceDiagram
    participant Vite
    participant FS as Filesystem
    participant Browser

    Note over Vite: Startup Check
    Vite->>FS: Check config/private.ts exists?
    FS-->>Vite: ❌ Not found

    Note over Vite: Setup Mode Activated
    Vite->>FS: Create blank private.ts
    Vite->>FS: Create placeholder collections
    Vite->>FS: Compile any existing collections

    Note over Vite: Open Setup Wizard
    Vite->>Browser: Open http://localhost:5173/setup

    Note over Browser: User sees setup wizard

Created config/private.ts:

export const privateEnv = createPrivateConfig({
  DB_TYPE: "mongodb",
  DB_HOST: "", // ← Empty values
  DB_PORT: 27017,
  DB_NAME: "", // ← Empty values
  DB_USER: "",
  DB_PASSWORD: "",
  JWT_SECRET_KEY: "", // ← Empty
  ENCRYPTION_KEY: "", // ← Empty
  MULTI_TENANT: false,
});

Phase 2: Server Hooks (Runtime)

File: src/hooks.server.ts

📚 For detailed middleware architecture, see Server Hooks & Middleware

flowchart TB
    A[Incoming Request] --> B{handleSystemState}
    B -->|System: IDLE| C{handleSetup}

    C -->|Check Config| D{private.ts valid?}
    D -->|No DB creds| E[isSetupComplete = false]

    E --> F{Path check}
    F -->|/setup| G[Allow Request]
    F -->|Other paths| H[Redirect to /setup]

    C -->|Check Database| I{hasAdminUsers?}
    I -->|No| E
    I -->|Yes| J[Allow Request - Setup Complete]

    style E fill:#d0d0d0,stroke:#333,stroke-width:3px
    style G fill:#e0e0e0,stroke:#333,stroke-width:2px
    style J fill:#e0e0e0,stroke:#333,stroke-width:2px

Phase 3: Setup GUI Interaction

File: src/routes/setup/+page.svelte

stateDiagram-v2
    [*] --> database-config: Step 1
    database-config --> DatabaseTest: User fills DB form
    DatabaseTest --> SystemLanguage: Test passed ✓
    SystemLanguage --> SeedData: Language selected
    SeedData --> AdminUser: Database seeded ✓
    AdminUser --> Complete: Admin form submitted
    Complete --> [*]: Redirect to CMS

    DatabaseTest --> database-config: Test failed ✗

    note right of DatabaseTest
        POST /setup?/testDatabase
        - Validates connection
        - Installs drivers if needed
    end note

    note right of SeedData
        POST /setup?/seedDatabase
        - Writes private.ts
        - Seeds settings/themes (background)
        - Creates collections (background)
        - Polling: /api/system/health
    end note

    note right of Complete
        POST /setup?/completeSetup
        - Creates admin user
        - Initializes system
        - Sets session cookie
    end note

User completes setup wizard:

Step 1: Database Configuration
  ↓
Step 2: Database Test → /setup?/testDatabase
  ↓
Step 3: System Language Selection
  ↓
Step 4: Seed Data → /setup?/seedDatabase
  ↓
Step 5: Admin User Creation → /setup?/completeSetup

Phase 4: Database Test (Server Function)

Action: src/routes/setup/+page.server.ts (Action: testDatabase)

sequenceDiagram
    participant Client as Browser
    participant Setup as /setup Actions
    participant PM as Package Manager
    participant Mongoose
    participant DB as MongoDB

    Client->>Setup: POST {host, port, name, user, pass}

    Note over Setup: Check Driver
    Setup->>Mongoose: Try import mongoose

    alt Driver Missing
        Setup->>PM: Detect package manager (bun/npm/pnpm/yarn)
        Setup->>PM: Install mongoose
        alt Permission Error
            PM-->>Setup: EACCES ✗
            Setup-->>Client: {success: false, manualCommand: "..."}
        else Success
            PM-->>Setup: Installation complete ✓
        end
    end

    Note over Setup: Build Connection
    Setup->>Setup: buildConnectionString()

    Note over Setup: Test Connection
    Setup->>DB: Connect with 15s timeout

    alt Success
        DB-->>Setup: Connected ✓
        Setup->>DB: Authenticate
        DB-->>Setup: Auth OK ✓
        Setup->>DB: Get database stats
        DB-->>Setup: {collections: 0, dataSize: 0}
        Setup-->>Client: {success: true, details: {...}}
    else Failure
        DB-->>Setup: Connection error
        Setup-->>Client: {success: false, error: "..."}
    end
POST /setup?/testDatabase
Body: {
  type: "mongodb",
  host: "localhost",
  port: 27017,
  name: "sveltycms",
  user: "admin",
  password: "secret123"
}

Process:

  1. Detects package manager (bun/npm/pnpm/yarn)
  2. Installs MongoDB driver if missing (mongoose)
  3. Builds connection string
  4. Tests connection with 15s timeout
  5. Returns detailed validation results

Response:

{
  "success": true,
  "message": "Connection successful",
  "details": {
    "authenticated": true,
    "dbStats": { "collections": 0, "dataSize": 0 },
    "warnings": []
  }
}

Phase 5: Seed Database

Action: /setup?/seedDatabase (+page.server.ts → setup.server.ts)

Called when user clicks “Next” after successful DB test. This process now runs in the background to prevent UI timeouts and allow real-time progress tracking.

┌─────────────────────────────────────────────────────┐
│ 1. Write config/private.ts                         │
│    - Writes DB credentials from test step          │
│    - Generates JWT_SECRET_KEY (32 bytes base64)    │
│    - Generates ENCRYPTION_KEY (32 bytes base64)    │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 2. Initialize database adapter (setup mode)        │
│    - getSetupDatabaseAdapter(dbConfig)             │
│    - Creates MongoDBAdapter                        │
│    - Connects to database                          │
│    - Calls setupAuthModels()                       │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 3. Seed default data (seed.ts)                     │
│    A. seedSettings(adapter)                        │
│       - Creates system_settings collection         │
│       - Seeds defaultPublicSettings (53 keys)      │
│       - Seeds defaultPrivateSettings (23 keys)     │
│    B. seedDefaultTheme(adapter)                    │
│       - Creates system_themes collection           │
│       - Seeds Default theme                         │
│    C. seedCollectionsForSetup(adapter)             │
│       - Scans .compiledCollections folder           │
│       - Creates collection models in MongoDB       │
│       - Reports progress via setupManager          │
│       - Returns firstCollection info               │
└─────────────────────────────────────────────────────┘

Key Function: seedCollectionsForSetup()

// File: src/routes/setup/seed.ts

export async function seedCollectionsForSetup(dbAdapter) {
  // 1. Scan filesystem for compiled collections
  const { scanCompiledCollections } = await import("@src/content/collectionScanner");
  const collections = await scanCompiledCollections();

  // 2. Register each collection as a model
  for (const schema of collections) {
    await dbAdapter.collection.createModel(schema);
    // ⚠️ Creates MongoDB collection: collection_<uuid>
  }

  // 3. Return first collection for fast redirect
  return { firstCollection: { name: "Posts", path: "/Collections/Posts" } };
}

Database Collections Created:

system_settings          ← Public/private settings
system_themes            ← Theme configurations
system_content_structure ← Navigation/hierarchy (empty for now)
collection_<uuid_1>      ← First collection (e.g., Posts)
collection_<uuid_2>      ← Second collection (e.g., Names)

Phase 6: Complete Setup

Action: /setup?/completeSetup (+page.server.ts → setup.server.ts)

Called when user submits admin user form.

┌─────────────────────────────────────────────────────┐
│ 1. Parse private.ts from filesystem                 │
│    - Bypasses Vite's import cache                  │
│    - Uses regex to extract config values           │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 2. Create setup database adapter                   │
│    - getSetupDatabaseAdapter(dbConfig)             │
│    - Creates Auth instance                         │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 3. Create admin user + session (single transaction)│
│    - setupAuth.createUserAndSession({              │
│        username, email, password,                  │
│        role: 'admin', isRegistered: true           │
│      })                                            │
│    - Inserts into auth_users collection           │
│    - Creates session in auth_sessions collection  │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 4. Initialize global system (db.ts)               │
│    - initializeWithFreshConfig()                   │
│    - Reloads private.ts from filesystem            │
│    - Connects global dbAdapter                     │
│    - Loads settings from database                  │
│    - Initializes ThemeManager                      │
│    - Initializes MediaFolder                       │
│    - State: IDLE → INITIALIZING → READY           │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 5. Initialize Content System                       │
│    - contentSystem.initialize(undefined, true)    │
│    - Loads collections from database               │
│    - Builds content structure cache                │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 6. Invalidate caches                               │
│    - invalidateSettingsCache()                     │
│    - invalidateSetupCache()                        │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 7. Store SMTP settings (no mail sent here)         │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 8. Set session cookie                              │
│    - Creates httpOnly session cookie               │
│    - Sets theme cookies                            │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 9. Return redirect path                            │
│    - Uses firstCollection from seed step (fast!)  │
│    - Redirects to /{lang}/{firstCollection.path}  │
│    - NO SERVER RESTART REQUIRED! ✨                │
└─────────────────────────────────────────────────────┘

Response (setup.server.ts):

{
  "success": true,
  "message": "Setup completed",
  "sessionId": "…",
  "sessionCookie": {
    "name": "…",
    "value": "…",
    "attributes": { "path": "/", "httpOnly": true, "sameSite": "lax", "maxAge": 86400 }
  },
  "publicSettings": {
    "SITE_NAME": "…",
    "DEFAULT_LANGUAGE": "en",
    "MULTI_TENANT": false,
    "DEMO": false,
    "USE_REDIS": false
  },
  "hardware": { "…": "host profile the run tuned itself to" },
  "redirectPath": "/en/Collections/Posts"
}

Setup Wizard Internals {#setup-wizard-internals}

How the wizard is built and what it keeps in memory while it runs. The operator procedure is the Setup wizard guide.

Component Structure

src/routes/setup/
├── +page.svelte               # Main orchestrator UI (Svelte 5 Runes)
├── +page.server.ts            # Consolidated setup actions and DB orchestration
├── setup.remote.ts            # Remote functions: test, seed, complete, email, Redis, driver install
├── database-config.svelte     # Step 1: Database setup, connection test, driver install
├── admin-config.svelte        # Step 2: Admin user with Strength-Validation
├── system-config.svelte       # Step 3: System settings, i18n, preset carousel
├── email-config.svelte        # Step 4: SMTP (optional)
└── review-config.svelte       # Step 5: Review & zero-latency completion

System Hardening:
├── src/databases/db-init.ts   # Topological Phased Boot engine
└── src/databases/config-state.ts # Security-masked configuration loader

State-Aware Initialization

This separation prevents the “Initialization Storm” where multiple requests try to boot the database simultaneously. The Turbo Pipeline (introduced in v0.0.8) ensures that setup checks consume zero CPU/IO cycles once the system is READY.

High-Performance Lifecycle

SveltyCMS employs a “State-Aware” initialization strategy to ensure the setup wizard is ultra-responsive while the production environment remains enterprise-grade.

sequenceDiagram
    participant V as Vite / Build
    participant H as hooks.server.ts
    participant D as db-init.ts
    participant U as User

    Note over V, U: PHASE 1: FRESH INSTALL
    V->>H: __SVELTY_SETUP_COMPLETE__ = false
    H->>H: Select "Bootstrap Pipeline" (Minimal)
    H->>U: Redirect to /setup
    U->>H: POST /setup (Complete)
    H->>V: Write config/private.ts
    V->>V: Hot Restart / Reload

    Note over V, U: PHASE 2: PRODUCTION BOOT
    V->>H: __SVELTY_SETUP_COMPLETE__ = true
    H->>H: Select "Full Pipeline" (Enterprise)
    H->>D: Trigger ensureFullInitialization()
    D->>D: Topological Phased Boot (READY)
    H->>U: Serve Dashboard (Full Speed)

    Note over V, U: PHASE 3: STEADY STATE
    U->>H: Incoming Request
    H->>H: Fast Check: __SVELTY_SETUP_COMPLETE__ (Zero I/O)
    H->>H: Execute Turbo Pipeline (~0.05ms)

Pipeline latency benchmarks

Based on tests/benchmarks/hooks-performance.test.ts, the setup-aware gating achieves significantly lower latency for fresh installs:

Pipeline Features Avg Latency Context
Bootstrap Health, Security, Compression < 0.1ms During /setup phase
Turbo Fast-path routing, Assets ~0.05ms Production steady-state
Full Auth, Audit, Content, SDK ~1.2ms Standard API operations
**Zero-I/O Optimization**: By injecting the setup status via Vite's `define` constant, we eliminate the need to check for `private.ts` on every request. This reduces I/O pressure on the filesystem, especially critical for high-concurrency environments.

On completion, nothing restarts. The completion step invalidates the settings cache, clears and reloads the private config in-process, and drops the setup cache; the wizard then hard-redirects. In development, writing config/private.ts additionally makes the Vite plugin send a full-reload to the browser.

Security Hardening

The setup process includes several enterprise-grade security features:

  • Log Sanitization: Sensitive values (passwords, connection strings) are excluded from structured log output during setup operations.
  • Secret Redaction: The configuration loader (config-state.ts) masks connection strings before logging them.
  • Write-Mutex Protection: SQLite writes during high-pressure setup are serialized by the adapter’s single-writer mutex (src/databases/sqlite/adapter-core.ts), so parallel seeding cannot produce “Database is locked” errors.

Setup Store State {#security-state-mgmt}

// Central state via setup-store (Svelte 5 runes)
{
  wizard: {
    dbConfig: {
      type: 'sqlite',
      host: 'config/database',
      port: '',
      name: 'sveltycms.db',
      user: '',
      password: '',
      replicaUrls: []
    },

    adminUser: {
      username: '',
      email: '',
      password: '',
      confirmPassword: ''
    },

    systemSettings: {
      siteName: 'SveltyCMS',
      hostProd: 'https://localhost:5173',
      defaultSystemLanguage: 'en',
      systemLanguages: ['en', 'de'],
      defaultContentLanguage: 'en',
      contentLanguages: ['en', 'de'],
      mediaStorageType: 'local',
      mediaFolder: './mediaFolder',
      preset: 'website',
      passwordMinLength: 8,
      timezone: 'UTC',
      useRedis: false,
      redisHost: 'localhost',
      redisPort: '6379',
      redisPassword: '',
      multiTenant: false,
      demoMode: false,
      cfApiToken: '',
      cfZoneId: '',
      cfPurgeMode: 'tags'
    },

    emailSettings: {
      smtpConfigured: false,
      skipWelcomeEmail: true,
      host: '',
      port: '587',
      user: '',
      password: '',
      from: '',
      secure: false
    },

    currentStep: 0,
    highestStepReached: 0,
    dbTestPassed: false,
    lastDbTestResult: null,
    lastTestFingerprint: null,
    isSeeding: false,
    redisTestPassed: false
  }
}

setup-store Architecture {#state-management-2}

File: src/stores/setup-store.svelte.ts

Pattern: Svelte 5 class-based store with runes

function createSetupStore() {
  const wizard = $state({
    dbConfig: { ... },
    adminUser: { ... },
    systemSettings: { ... },
    emailSettings: { ... },
    currentStep: 0,
    highestStepReached: 0,
    dbTestPassed: false,
    validationErrors: {},
    stepErrors: { ... },
    isLoading: false,
    isSubmitting: false,
  });

  // Methods
  function load() {
    // rehydrate each section from localStorage
  }

  function clear() {
    // reset to defaults
  }

  function setupPersistence() {
    $effect(() => {
      if (!isLoaded || !storage) return;
      storage.setItem(KEYS.db, JSON.stringify(wizard.dbConfig));
      // … one key per section, plus step / highestStep / dbTestPassed
    });
  }

  return {
    wizard,
    load,
    clear,
    setupPersistence
  };
}

export const setupStore = createSetupStore();

Derived state is computed, never stored: stepCompleted, stepClickable (highestStepReached >= n), canProceed (per-step gate, including the Redis test), passwordRequirements, and dbConfigChangedSinceTest (a fingerprint comparison against the last successful test).

Persistence Strategy

Keys: prefix setupWizard: — dbConfig, adminUser, systemSettings, emailSettings, currentStep, highestStep, dbTestPassed.

When Data is Saved:

  • Automatically on every state change (via $effect)
  • Only in the browser (localStorage), and only when a storage probe succeeds

When Data is Loaded:

  • On component mount, for a browser session that already carries the sveltycms_setup_active flag — a refresh, or navigating back in the same session
  • A brand-new browser session starts from defaults: the store is cleared on first entry

When Data is Cleared:

  • Through the store’s clear() method (reset to defaults)
  • Never on validation errors (user input is preserved)
  • Not on completion — the wizard hard-redirects and the keys are left behind

Unsaved Changes Warning

Implementation (src/routes/setup/+page.svelte):

// Capture initial state on mount, after load()/clear()
let initialDataSnapshot = $state<string>("");

onMount(() => {
  initialDataSnapshot = JSON.stringify(wizard);

  // Warn before navigation
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (hasUnsavedChanges() && !wizard.isSubmitting) {
      e.preventDefault();
    }
  };

  window.addEventListener("beforeunload", handleBeforeUnload);
  return () => window.removeEventListener("beforeunload", handleBeforeUnload);
});

// Check if data changed
const hasUnsavedChanges = $derived(() => {
  if (!initialDataSnapshot) return false;
  return JSON.stringify(wizard) !== initialDataSnapshot;
});

Behavior:

  • Shows browser warning if user tries to close/refresh with unsaved changes
  • Suppressed during final submission (wizard.isSubmitting = true)
  • Cross-browser compatible

Validation Schemas

Located in src/utils/schemas.ts and shared with login, password reset, and SMTP configuration:

import * as v from "valibot";

const dbTypeSchema = picklist(
  ["mongodb", "mongodb+srv", "postgresql", "mysql", "mariadb", "sqlite"],
  "Invalid database type",
);

// Database configuration — strictObject blocks extra field injection
export const dbConfigSchema = pipe(
  strictObject({
    type: dbTypeSchema,
    host: dbHostSchema, // required, max 255
    port: dbPortSchema, // optional, 1-65535
    name: dbNameSchema, // required, letters/numbers/._- only
    user: dbUserSchema,
    password: dbPasswordSchema,
    replicaUrls: optional(array(string())),
  }),
  check((input) => {
    const requiresAuth = ["postgresql", "mysql", "mariadb", "mongodb+srv"];
    return (
      !requiresAuth.includes(input.type) ||
      ((input.user?.length ?? 0) > 0 && (input.password?.length ?? 0) > 0)
    );
  }, "Username and password are required for this database type."),
);

The wizard validates three schemas per step — dbConfigSchema, setupAdminSchema, systemSettingsSchema — and smtpConfigSchema covers the optional email step.

Password rules have two layers: passwordSchema (inside setupAdminSchema) requires the configured minimum length plus a letter, a number, and a special character, while Auth.validatePasswordStrength() — which runs in Auth.createUser() at completion — additionally requires an uppercase and a lowercase letter.

Dual Validation Approach

The wizard implements a two-tier validation system:

1. Client-Side Validation (Real-time)

Technology: Valibot schemas with Svelte 5 runes

Implementation:

// In each step component (e.g., database-config.svelte)
import { safeParse } from "valibot";
import { dbConfigSchema } from "@utils/schemas";

// Real-time validation state
let localValidationErrors = $state<Record<string, string>>({});

// Validate form data in real-time
const validationResult = $derived(
  safeParse(dbConfigSchema, {
    type: dbConfig.type,
    host: dbConfig.host,
    port: dbConfig.port,
    name: dbConfig.name,
    user: dbConfig.user,
    password: dbConfig.password,
  }),
);

const isFormValid = $derived(validationResult.success);

// Update errors when validation changes
$effect(() => {
  const newErrors: Record<string, string> = {};
  if (!validationResult.success) {
    for (const issue of validationResult.issues) {
      const path = issue.path?.[0]?.key as string;
      if (path) {
        newErrors[path] = issue.message;
      }
    }
  }
  localValidationErrors = newErrors;
});

// Combine local and parent validation errors
const displayErrors = $derived<Record<string, string>>({
  ...localValidationErrors,
  ...validationErrors, // Server errors take precedence
});

Benefits:

  • Instant feedback as users type
  • No server round-trip needed
  • Reduces form submission errors
  • Better user experience

The step components only surface a local error once the field has been touched (blurred), then let parent/server errors win — see touchedFields in admin-config.svelte.

2. Server-Side Validation

Location: the store (src/stores/setup-store.svelte.ts)

Implementation: steps 0-2 map to a schema; the validator clears only the errors for the fields it is about to check, then merges the new ones, so unrelated errors survive:

const validationMap: Record<number, { schema: unknown; data: () => unknown }> = {
  0: { schema: dbConfigSchema, data: () => wizard.dbConfig },
  1: { schema: setupAdminSchema, data: () => wizard.adminUser },
  2: { schema: systemSettingsSchema, data: () => wizard.systemSettings },
  // Steps 3 (Email) and 4 (Review) have no validation and are treated as valid
};

function validateStep(step: number, mutateErrors = true): boolean {
  const validationEntry = validationMap[step];
  if (!validationEntry) return true;

  const { schema, data: getData } = validationEntry;
  const result = safeParse(schema, getData());
  // … delete the errors for the validated keys, then merge the new issues
}

Benefits:

  • Final validation before API calls
  • Server-authoritative (client can be bypassed)
  • Consistent with API validation
  • Security checkpoint

Error Display Pattern

Each input field follows this pattern (the shared <Input> component implements the same contract):

<input
	id="db-host"
	bind:value={dbConfig.host}
	type="text"
	class="w-full rounded border border-surface-300 px-3 py-2 {displayErrors.host ? 'border-error-500' : ''}"
	aria-invalid={!!displayErrors.host}
	aria-describedby={displayErrors.host ? 'db-host-error' : undefined}
/>
{#if displayErrors.host}
	<div id="db-host-error" class="mt-1 text-xs text-error-500" role="alert">
		{displayErrors.host}
	</div>
{/if}

Accessibility Features:

  • aria-invalid indicates validation state
  • aria-describedby links to error message
  • role="alert" announces errors to screen readers
  • Visual red border for quick identification

Error Recovery Strategies

Retrying a failed test {#auto-retry}

testDatabaseConnection() calls the remote function exactly once and surfaces the result — there is no client-side retry loop. On failure the step renders the classification, the hint, and the detail block; the retry is the user pressing Test Connection again.

// src/stores/setup-store.svelte.ts — single call, no retry loop
const data = await testDbRemote({
  configData: wizard.dbConfig,
  createIfMissing,
  allowOverwrite,
  // the wizard owns this DB once a test has succeeded for this exact config
  ownsDatabase: wizard.dbTestPassed && !dbConfigChangedSinceTest,
});
wizard.lastDbTestResult = data;
if (data?.success) {
  wizard.dbTestPassed = true;
  wizard.lastTestFingerprint = dbConfigFingerprint;
}

Graceful Degradation

Completion keeps going when a side effect fails. SMTP values are written to settings but no mail is sent at completion; the Website Starter plugin auto-enable, the session-cache priming, and the first-collection cache lookup are each best-effort and log a warning instead of failing the run. A failed completion itself returns success: false with a message, leaving the wizard in place with the entered data intact.

User Guidance

classifyDatabaseError() (src/routes/setup/error-classifier.ts) maps raw driver errors to a classification, a user-facing sentence, and a hint:

if (/authentication failed|bad auth|not authorized|bad login/i.test(lower)) {
  return {
    classification: "AUTH_FAILED",
    userFriendly: "Authentication failed. Please check your username and password.",
    hint: "The provided user does not have permission to access this database or the password is incorrect.",
  };
}
// … CONNECTION_REFUSED (refused / DNS / timeout / not allowlisted),
// DB_NOT_FOUND, DB_ALREADY_EXISTS, CASE_MISMATCH, INVALID_CONFIG

Two flags drive the follow-up dialogs: dbDoesNotExist offers to create the database, canOverwrite offers to drop and recreate it.

DB_ALREADY_EXISTS (“Database not empty”) is only raised for a foreign database. A successful test runs ensureAuth() / ensureSystem() / … and the store then auto-seeds, so the wizard’s own database is non-empty on the next test. The guard is therefore skipped when the caller reports it already owns that exact config (ownsDatabase) — re-testing your own initialised database succeeds instead of offering to overwrite it.

Adding a New Step

  1. Create Step Component
<!-- src/routes/setup/my-new-step.svelte -->
<script lang="ts">
	import type { MyStepData } from '@stores/setup-store.svelte';

	let { stepData = $bindable(), validationErrors } = $props<{
		stepData: MyStepData;
		validationErrors: Record<string, string>;
	}>();
</script>

<div class="fade-in">
	<p class="mb-8 text-center">Step description</p>

	<div class="space-y-6">
		<!-- Your form fields -->
	</div>
</div>
  1. Add Validation Schema
// src/utils/schemas.ts
export const myStepSchema = v.object({
  field1: v.pipe(v.string(), v.minLength(1, "Field 1 is required")),
  field2: v.pipe(v.string(), v.email("Invalid email")),
});
  1. Update setup-store
// src/stores/setup-store.svelte.ts
class SetupStore {
  wizard = $state({
    // ... existing fields
    myStepData: {
      field1: "",
      field2: "",
    },
  });
}
  1. Register in Main Wizard — one entry per step, labelled from Paraglide messages, then the store’s validationMap gets a case:
<!-- src/routes/setup/+page.svelte -->
<script lang="ts">
	import MyNewStep from './my-new-step.svelte';

	const steps = $derived([
		{ label: setup_step_database(), shortDesc: setup_step_database_desc() },
		// … one entry per step
	]);
</script>

Extending Validation

Add Cross-field Validation:

export const adminSchema = v.object(
  {
    password: v.string(),
    confirmPassword: v.string(),
  },
  [
    v.forward(
      v.partialCheck(
        [["password"], ["confirmPassword"]],
        (input) => input.password === input.confirmPassword,
        "Passwords must match",
      ),
      ["confirmPassword"],
    ),
  ],
);

Testing {#validation-testing}

Unit Tests (Vitest):

import { describe, it, expect } from "vitest";
import { safeParse } from "valibot";
import { dbConfigSchema } from "@utils/schemas";

describe("dbConfigSchema", () => {
  it("validates correct database config", () => {
    const result = safeParse(dbConfigSchema, {
      type: "mongodb",
      host: "localhost",
      port: "27017",
      name: "test",
      user: "admin",
      password: "secret",
    });

    expect(result.success).toBe(true);
  });

  it("rejects empty host", () => {
    const result = safeParse(dbConfigSchema, {
      type: "mongodb",
      host: "",
      port: "27017",
      name: "test",
      user: "",
      password: "",
    });

    expect(result.success).toBe(false);
    expect(result.issues[0].message).toContain("Host is required");
  });
});

Test Suites {#testing-2}

Suite Path Covers
Store tests/unit/stores/setup-store.test.ts Steps, persistence, password requirements
Utilities tests/unit/api/setup-utils.test.ts Connection strings, error classification
Presets tests/unit/setup/preset-collections.test.ts, tests/integration/setup-presets.test.ts Blueprint to collection files
Locales tests/unit/setup/language-packs.test.ts, tests/unit/setup/sync-inlang-locales.test.ts System languages, inlang sync
Hooks tests/unit/hooks/setup.test.ts Setup gating in the request pipeline
Integration tests/integration/setup-wizard.test.ts, tests/integration/api/setup-actions.test.ts Real SQLite, end to end

Run them with bun run test:unit, bun run test:integration (builds the harness first), and bun run test:e2e.

End-to-End {#automated-testing-e2e}

tests/e2e/routes/setup/setup-wizard.spec.ts drives the wizard in a browser: it starts at /, expects the redirect to /setup, clears storage, walks the steps, and waits for the post-setup redirect. It runs in the e2e-prep chain (wizard → first user → auth setup) that seeds tests/e2e/.auth/admin.json before the chromium shards start, so it is never part of a --grep group.

Wizard UI Troubleshooting

Issue: Validation Errors Not Showing

Symptoms: Input changes but no error messages appear

Cause: Missing $effect hook or incorrect error prop passing

Solution:

// Ensure $effect updates localValidationErrors
$effect(() => {
  const newErrors: Record<string, string> = {};
  if (!validationResult.success) {
    for (const issue of validationResult.issues) {
      const path = issue.path?.[0]?.key as string;
      if (path) {
        newErrors[path] = issue.message;
      }
    }
  }
  localValidationErrors = newErrors;
});

// Ensure displayErrors combines both sources
const displayErrors = $derived<Record<string, string>>({
  ...localValidationErrors,
  ...validationErrors,
});

Issue: Form Data Not Persisting

Symptoms: Data lost on page refresh

Cause: setupPersistence() not called, or localStorage unavailable

Solution:

onMount(() => {
  // Fresh session clears; a reload in the same session loads
  if (!sessionStorage.getItem('sveltycms_setup_active')) {
    clearStore();
    sessionStorage.setItem('sveltycms_setup_active', 'true');
  } else {
    loadStore();
  }

  // Capture initial state AFTER loading
  initialDataSnapshot = JSON.stringify(wizard);
  setupPersistenceFn();
});

Issue: Duplicate Submissions

Symptoms: Multiple API calls on button click

Cause: Missing isSubmitting guard

Solution:

async function completeSetup() {
  // Guard at the top
  if (wizard.isSubmitting) {
    return false;
  }

  wizard.isSubmitting = true;

  try {
    // ... API call
  } finally {
    wizard.isLoading = false;
    wizard.isSubmitting = false; // Always reset
  }
}

Issue: Unsaved Changes Warning Not Working

Symptoms: No warning when closing browser with unsaved data

Cause: Missing beforeunload handler or incorrect change detection

Solution:

onMount(() => {
  // Capture initial state after loading from localStorage
  initialDataSnapshot = JSON.stringify(wizard);

  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (hasUnsavedChanges() && !wizard.isSubmitting) {
      e.preventDefault();
    }
  };

  window.addEventListener("beforeunload", handleBeforeUnload);

  return () => {
    window.removeEventListener("beforeunload", handleBeforeUnload);
  };
});

Issue: Step Navigation Broken

Symptoms: Cannot click on completed steps

Cause: stepClickable derived state not updated

Solution:

const stepClickable = $derived.by<boolean[]>(() => [
  true, // Step 0 (Database) is always clickable
  wizard.highestStepReached >= 1, // Step 1 (Admin) is clickable if we've reached it
  wizard.highestStepReached >= 2,
  wizard.highestStepReached >= 3,
  wizard.highestStepReached >= 4,
]);

Server Restart Flow

Phase 1: Vite Startup (Build Time) {#phase-1-vite-startup-build-time-2}

File: vite.config.ts

┌─────────────────────────────────────────────────────┐
│ 1. Vite starts                                      │
│    - Checks if config/private.ts exists            │
│    - isSetupComplete() → true (config exists +     │
│      has valid DB credentials)                     │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 2. cmsWatcherPlugin() activates                     │
│    - Initializes collection structure              │
│    - Compiles collections intelligently            │
│    - Sets up file watchers                         │
└─────────────────────────────────────────────────────┘

Phase 2: Intelligent Collection Compilation

File: src/utils/compilation/compile.ts

The compilation system uses content hashing and UUID preservation to avoid unnecessary recompilation:

┌─────────────────────────────────────────────────────┐
│ 1. Scan existing compiled collections              │
│    - Builds map: path → { jsPath, uuid, hash }    │
│    - Extracts UUID and hash from JS files          │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 2. Scan source collections (config/collections)    │
│    - Gets list of .ts files                        │
│    - Calculates content hash for each              │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 3. Smart recompilation decision                    │
│    For each collection:                            │
│    IF hash matches existing compiled file          │
│      → SKIP (no changes detected)                  │
│      → PRESERVE existing UUID                      │
│    ELSE                                            │
│      → RECOMPILE                                   │
│      → PRESERVE existing UUID (if found)           │
│      → GENERATE new UUID (if new file)             │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 4. AST transformation                              │
│    - Converts TypeScript to JavaScript             │
│    - Adds .js extensions to imports                │
│    - Injects _id (UUID) into schema                │
│    - Injects __HASH__ comment                      │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 5. Cleanup orphaned files                          │
│    - Removes .js files without .ts source          │
│    - Removes empty directories                     │
└─────────────────────────────────────────────────────┘

Example Compiled Collection:

// .compiledCollections/Collections/Posts.js
// __HASH__:a7f3e9c2d8b1f4a6e3c9d2b8f1a4e6c3

export const schema = {
  _id: "550e8400-e29b-41d4-a716-446655440000", // ← UUID preserved!
  name: "Posts",
  icon: "bi:file-text",
  fields: [
    { label: "Title", db_fieldName: "title", widget: "text" },
    { label: "Content", db_fieldName: "content", widget: "richtext" },
  ],
};

Phase 3: Server Initialization

File: src/hooks.server.ts + src/databases/db.ts

┌─────────────────────────────────────────────────────┐
│ handleSystemState                                   │
│  - System state: IDLE                              │
│  - First request triggers: await dbInitPromise     │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ initializeOnRequest() (db.ts)                       │
│  1. Load private config (privateEnv)               │
│  2. Connect to database                            │
│  3. Load adapters (MongoDBAdapter)                 │
│  4. Setup auth models                              │
│  5. Load settings from database                    │
│  6. Initialize default theme                       │
│  7. Initialize theme manager                       │
│  8. Initialize media folder                        │
│  9. Initialize virtual folders                     │
│  State: IDLE → INITIALIZING → READY               │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ Content System auto-initializes on first access    │
│  - Scans .compiledCollections                       │
│  - Registers collection models in database         │
│  - Builds content structure cache                  │
└─────────────────────────────────────────────────────┘

Collection UUID Management

UUID Assignment Strategy

The Problem: Collections need stable, persistent IDs for:

  • Database table names (collection_<uuid>)
  • Content references (foreign keys)
  • API routes
  • Permissions/roles

The Solution: UUIDs are assigned once at compilation and preserved across restarts.

UUID Lifecycle

┌─────────────────────────────────────────────────────┐
│ 1. New Collection Created                          │
│    config/collections/Posts.ts                     │
│    export const schema = { name: "Posts", ... }    │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 2. First Compilation                               │
│    - Generates new UUID                            │
│    - Injects into schema._id                       │
│    - Saves to .compiledCollections/Collections/     │
│      Posts.js                                      │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 3. Collection Modified                             │
│    config/collections/Posts.ts changed             │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 4. Smart Recompilation                             │
│    - Detects hash change                           │
│    - PRESERVES existing UUID from compiled file    │
│    - Recompiles with same UUID                     │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│ 5. Server Restart                                  │
│    - Reads compiled collections                    │
│    - UUIDs remain stable                           │
│    - Database tables remain consistent             │
└─────────────────────────────────────────────────────┘

UUID Extraction Logic

File: src/utils/compilation/compile.ts

// Extract UUID from compiled JS file
function extractUUIDFromJs(content: string): string | null {
  const match = content.match(new RegExp("_id:\\s*" + "[\"']" + "([^\"'\\s]+)" + "[\"']"));
  return match ? match[1] : null;
}

// Extract hash from compiled JS file
function extractHashFromJs(content: string): string | null {
  const match = content.match(/__HASH__:(\w+)/);
  return match ? match[1] : null;
}

AST Transformation:

// Injects UUID into schema during compilation
const schemaUuidTransformer =
  (uuid: string): ts.TransformerFactory<ts.SourceFile> =>
  (context) =>
  (sourceFile) => {
    // Finds: export const schema = { ... }
    // Injects: _id: "uuid-here"
    // Result: export const schema = { _id: "uuid-here", ... }
  };

Key Components

1. Configuration Files

File Purpose Created When
config/private.ts Database credentials, security keys Vite startup (empty) → Seed step (populated)
config/collections/*.ts Collection definitions (TypeScript) User-created
.compiledCollections/**/*.js Compiled collections (JavaScript + UUID) Build time

2. API Endpoints

Endpoint Purpose Called When
/setup?/testDatabase Validates DB connection Setup wizard (Step 2)
/setup?/seedDatabase Seeds settings, themes, collections Setup wizard (Step 4)
/setup?/completeSetup Creates admin user, initializes system Setup wizard (Step 5)

3. Hooks & Middleware

Hook Purpose Order
handleSystemState Blocks requests if system FAILED 1st
handleSetup Redirects to /setup if incomplete 2nd
handleAuthentication Validates session, identifies user 6th

4. Core Services

Service Purpose File
dbAdapter Database abstraction layer src/databases/db.ts
contentSystem Public system facade src/content/index.ts
Auth Authentication/authorization src/databases/auth/index.ts
SettingsService Settings cache management src/services/core/settings-service.ts

State Machine

System States

IDLE → INITIALIZING → READY → DEGRADED → FAILED
  ↑                      ↓
  └──────────────────────┘
         (reinit)
State Description Allowed Routes
IDLE No valid config or not initialized /setup, /api/system/health, /login, /static, /.well-known
INITIALIZING Loading config, connecting to DB /api/system/health
READY Fully operational All routes
DEGRADED Some services failed, but operational All routes (with warnings)
FAILED Critical failure, unusable None (503 errors)

Transition Triggers

// File: src/stores/system/state.svelte.ts:395 — the single state-transition entry point

export function setSystemState(state: SystemState, reason?: string): void {
  const now = Date.now();

  system.update((current) => {
    const transition = {
      from: current.overallState,
      to: state,
      timestamp: now,
      ...(reason && { reason }),
    };

    // Keep last 50 transitions for analysis
    const stateTransitions = [...current.performanceMetrics.stateTransitions, transition].slice(
      -50,
    );
    // …
  });
}

Service-level health is reported through updateServiceHealth() (state.svelte.ts:345), and the filesystem-backed bootstrap config is (re)read by loadPrivateConfig(forceReload) (src/databases/config-state.ts:62) — not by Vite’s module graph.


Flow Diagrams

First-Time Setup (Complete Flow)

sequenceDiagram
    participant V as Vite
    participant H as Hooks
    participant U as User (Browser)
    participant API as Setup API
    participant DB as Database
    participant CS as Content System

    V->>V: Check private.ts (missing)
    V->>V: Create blank private.ts
    V->>V: Compile collections
    V->>U: Open /setup in browser

    U->>**API Call**: `POST /setup?/testDatabase` (SvelteKit Server Action)
    API->>DB: Test connection
    DB-->>API: Success
    API-->>U: Connection valid

    U->>API: POST /setup?/seedDatabase
    API->>API: Write private.ts (with credentials)
    API->>DB: Seed settings
    API->>DB: Seed themes
    API->>DB: Seed collections (create models)
    API-->>U: Seed complete + firstCollection

    U->>API: POST /setup?/completeSetup
    API->>DB: Create admin user + session
    API->>API: initializeWithFreshConfig()
    API->>CS: Initialize Content System
    API->>API: Invalidate caches
    API-->>U: Setup complete + redirect + session cookie

    U->>H: GET /en/Collections/Posts
    H->>H: handleSetup: isSetupComplete() → true
    H->>CM: Load collection
    CM-->>U: Render collection view

Server Restart (Smart Compilation)

sequenceDiagram
    participant V as Vite
    participant FS as Filesystem
    participant CM as Compilation
    participant DB as Database

    V->>V: Check private.ts (exists + valid)
    V->>CM: Trigger intelligent compilation

    CM->>FS: Scan .compiledCollections/
    FS-->>CM: {path → {jsPath, uuid, hash}}

    CM->>FS: Scan config/collections/
    FS-->>CM: List of .ts files

    loop For each collection
        CM->>CM: Calculate content hash
        alt Hash matches
            CM->>CM: SKIP (no changes)
        else Hash differs
            CM->>CM: RECOMPILE
            CM->>CM: PRESERVE UUID
            CM->>FS: Write updated .js file
        end
    end

    CM->>V: Compilation complete
    V->>DB: Initialize database connection
    V->>CS: Initialize Content System
    CM->>DB: Register collection models

Best Practices

For Developers

  1. Never manually edit compiled collections (.compiledCollections/)

    • Always edit source files (config/collections/)
    • Let the compilation system handle UUID preservation
  2. Don’t delete compiled collections during development

    • UUIDs would be regenerated, breaking database references
    • Use git clean carefully
  3. Use content hashing for change detection

    • The system automatically detects changes via SHA-256 hash
    • No manual intervention needed
  4. Validate on both sides — the wizard validates in the step component and again in the store and completeSetup(); never trust the client alone.

  5. Preserve user input — clear field errors, never the form. validateStep() deletes only the errors for the fields it re-checks.

  6. Keep state minimal — derived values (stepClickable, canProceed, passwordRequirements, dbConfigChangedSinceTest) are computed, not stored.

  7. Document new steps — update the Setup wizard guide when a step or field changes.

  8. Performance first — lazy-load step components and keep step transitions off the boot path.

For System Administrators

  1. Backup compiled collections before migrations

    tar -czf .compiledCollections.backup.tar.gz .compiledCollections/
  2. Database migrations preserve UUIDs

    • Collection IDs are stored in _id field
    • Never modify _id in database or compiled files
  3. Setup completion is atomic

    • If setup fails, system reverts to setup mode
    • No partial state left behind

Tests and CI

How the suite and the pipeline run is documented with the tests, not here:

Part IV: Troubleshooting & Reference {#body-part-iv}

Troubleshooting

Setup wizard loops infinitely

Cause: isSetupCompleteAsync() returns false due to missing users

Fix:

# Check if admin users exist
mongo sveltycms --eval "db.auth_users.find().pretty()"

# If empty, complete setup wizard again
# or manually create admin user via MongoDB shell

Collections not appearing after setup

Cause: seedCollectionsForSetup() failed silently

Fix:

# Check logs for collection seeding errors
tail -f logs/app.log | grep "seedCollections"

# Manually trigger collection initialization
# Restart server with DEBUG=true

UUID changed after recompilation

Cause: Compiled collection was deleted or corrupted

Fix:

# Restore from backup
tar -xzf .compiledCollections.backup.tar.gz

# Or manually restore UUID in MongoDB
mongo sveltycms --eval 'db.system_content_structure.find({name: "Posts"})'
# Copy _id, then update compiled file

Summary

This page covers the engine side of first run:

✅ First-Time Setup - Installer phases, the config they generate, and what completion triggers ✅ Server Restart - Intelligent recompilation and UUID preservation ✅ Setup Wizard Internals - Store, validation layers, error classification ✅ Collection Management - UUID assignment and lifecycle ✅ State Machine - IDLE → READY, with the transition triggers ✅ Troubleshooting - Boot, collection, and wizard-UI issues

Key Highlights

🚀 Zero-Restart Setup - System becomes operational without server restart 🔑 UUID Preservation - Collections maintain stable IDs across restarts ⚡ Smart Compilation - Only recompiles changed collections using content hashing 🧪 Comprehensive Testing - Setup suites across unit, integration, and E2E 🤖 Automated CI/CD - GitHub Actions for testing and releases 📝 Automated Scripts - setup-system.ts for headless CI/CD setup

⚠️ Known Issue: Collection UUID table creation may have race conditions during seedCollectionsForSetup()

The system is production-ready and fully automated for both local development and CI/CD environments.


Related Documentation

Architecture

API References

Database

Guides


Quick Reference

Key Files

File Purpose When Modified
vite.config.ts Build-time setup detection Rarely
src/hooks.server.ts Runtime request gatekeeper When adding middleware
src/databases/db.ts Database initialization When adding adapters
src/content/index.ts Collection management When changing collection logic
src/utils/compilation/compile.ts Smart compilation When changing collection format

Key Concepts

  • Setup Mode: System state when no admin users exist
  • UUID Preservation: Collections keep the same ID across restarts
  • Smart Compilation: Only recompiles changed collections using content hashing
  • Zero-Restart Setup: System becomes operational without server restart
  • State Machine: IDLE → INITIALIZING → READY → DEGRADED → FAILED

Performance Benchmarks

Operation Expected Time Notes
Vite Startup 2-5 seconds Includes TypeScript compilation
Database Test 1-3 seconds May include driver installation
Seed Database 3-8 seconds Depends on collection count
Complete Setup 4-6 seconds Includes full system init
Total Setup 10-25 seconds Excluding user input
Collection Compile 50-200ms per file With UUID preservation
Server Restart 3-5 seconds To READY state
architectureinitializationsetupworkflowcollectionsinstallationtestingci-cd
Was this page helpful?