Skip to content

Documentation

Login Resilience & Recovery

How SveltyCMS handles database errors and provides recovery options for users

7/7/2026
6 min read Edit on GitHub

This document describes the improved error handling system for login failures, particularly when the database is unavailable or empty.

Overview

The login system integrates with SveltyCMS’s centralized state management (@src/stores/system) to provide fast, intelligent error detection and user-friendly recovery options.

Architecture

1. State Management Integration

The login flow leverages the existing state management system for optimal performance:

// Check system state first - avoid unnecessary DB queries
const { getSystemState, isServiceHealthy } = await import("@src/stores/system");
const systemState = getSystemState();

// Fast path: System already knows it's in FAILED state
if (systemState.overallState === "FAILED") {
  // Return error immediately without waiting for timeout
  return showDatabaseError(lastFailure.reason);
}

// Fast path: Database service is marked unhealthy
if (!isServiceHealthy("database")) {
  return showDatabaseError(dbStatus.message);
}

Benefits:

  • Instant error detection - No 30s timeout waiting
  • 🎯 Precise error messages - State management tracks exact failure reason
  • 💾 Resource efficient - Reuses cached state instead of querying database

2. Database Health Check

After checking state management, a lightweight database verification confirms setup completion:

async function checkDatabaseHealth(): Promise<{ healthy: boolean; reason?: string }> {
  // Step 1: Check state management (instant, cached)
  const systemState = getSystemState();
  if (!isServiceHealthy("database")) {
    return { healthy: false, reason: dbStatus.message };
  }

  // Step 2: Verify database has data (quick query)
  const rolesResult = await dbAdapter.roles.getAll();
  if (rolesResult.data.length === 0) {
    return {
      healthy: false,
      reason: "Database is empty - setup may not have completed",
    };
  }

  return { healthy: true };
}

Why this approach?

  • State management catches infrastructure failures (connection lost, MongoDB down)
  • Database query catches configuration issues (empty DB after manual deletion)
  • Together they provide complete coverage

3. Reduced Timeout for Auth Service

The login page (src/routes/login/+page.server.ts) polls for the auth service with a tight timeout:

// Module-level constant in login/+page.server.ts
const AUTH_SERVICE_TIMEOUT_MS = 10_000; // 10 seconds

async function waitForAuthService(): Promise<boolean> {
  const startTime = Date.now();
  while (Date.now() - startTime < AUTH_SERVICE_TIMEOUT_MS) {
    if (auth && typeof auth.getUserCount === "function") return true;
    await new Promise((r) => setTimeout(r, 200));
  }
  return !!(auth && typeof auth.getUserCount === "function");
}

Rationale:

  • Previously: 30 seconds timeout (too long for user-facing login)
  • Now: 10 seconds (module-level constant AUTH_SERVICE_TIMEOUT_MS)
  • If database is healthy, auth initializes in <1 second
  • 10 seconds is 10x the normal initialization time
  • Failing fast (10s vs 30s) improves user experience

Error Flow

graph TD
    A[User visits /login] --> B{Check System State}
    B -->|FAILED| C[Show Error Dialog Immediately]
    B -->|Other| D[Check DB Health]
    D -->|Unhealthy| C
    D -->|Healthy| E[Wait for Auth Service 10s]
    E -->|Ready| F[Show Login Form]
    E -->|Timeout| G[Show Auth Not Ready Error]

    C --> H{User Action}
    H -->|Reset Setup| I[Delete config/private.ts]
    H -->|Refresh| J[Retry Health Check]
    I --> K[Redirect to /setup]

User-Facing Error Dialog

When database issues are detected, users see a comprehensive error dialog:

{#if data.showDatabaseError}
	<div class="error-dialog">
		<h2>Database Issue Detected</h2>
		<p>The system configuration exists, but the database is empty or unavailable.</p>

		<div class="error-reason">
			<p><strong>Reason:</strong></p>
			<p>{data.errorReason}</p>
		</div>

		<h3>Possible Solutions:</h3>
		<ul>
			<li>If MongoDB is not running, start it and refresh this page</li>
			<li>If the database was manually dropped, you need to reset the setup</li>
			<li>Check your database connection settings in config/private.ts</li>
			<li>Restore your database from a backup if available</li>
		</ul>

		<button onclick={resetSetup}>Reset Setup</button>
		<button onclick={refresh}>Refresh Page</button>
	</div>
{/if}

Recovery Options

Option 1: Refresh (Quick Fix)

Best for temporary issues:

  • Database was restarting
  • Network hiccup resolved
  • Database just came back online

Action: Click “Refresh Page” button

Option 2: Reset Setup (Nuclear Option)

When configuration is corrupted or database was manually deleted:

Security: Only allowed when:

  1. User is authenticated as admin, OR
  2. System is in FAILED state

Process:

  1. User clicks “Reset Setup”
  2. Confirmation dialog appears
  3. System deletes config/private.ts
  4. Clears cached configuration
  5. Redirects to /setup wizard

Implementation:

// In login/+page.server.ts — form action, not standalone endpoint
resetSetup: async ({ locals }) => {
  // Security check
  const systemState = getSystemState();
  const isAdmin = locals.user?.role === "admin";
  const isSystemFailed = systemState.overallState === "FAILED";

  if (!isAdmin && !isSystemFailed) {
    return fail(403, { message: "You do not have permission to reset the setup." });
  }

  // Delete config and clear cache
  await fs.unlink(path.join(process.cwd(), "config", "private.ts"));
  invalidateSetupCache(true);

  return { success: true, message: "Setup has been reset." };
};

Performance Metrics

Before Optimization

  • Error Detection Time: 30-60 seconds (timeout-based)
  • Database Queries: 3-5 redundant queries before timeout
  • User Experience: Frustrating wait with no feedback

After Optimization

  • Error Detection Time: <100ms (state-based) or 10s max (timeout)
  • Database Queries: 1 lightweight query (roles check)
  • User Experience: Immediate feedback with actionable solutions

Measurement

State management tracks these metrics automatically:

const systemState = getSystemState();

// Check database service metrics
const dbMetrics = systemState.services.database.metrics;
console.log({
  failureCount: dbMetrics.failureCount,
  consecutiveFailures: dbMetrics.consecutiveFailures,
  lastFailureAt: dbMetrics.lastFailureAt,
  uptimePercentage: dbMetrics.uptimePercentage,
});

Testing

Playwright Test Enhancement

// Wait for page load before checking for database form
await page.waitForLoadState("networkidle");

// Extended timeout for database configuration to appear
await expect(page.getByRole("heading", { name: /database/i })).toBeVisible({ timeout: 30000 });

Why networkidle?

  • Ensures all JavaScript has executed
  • State management has initialized
  • Error dialogs have rendered if needed

Troubleshooting

Scenario 1: “Database is empty” error but database is running

Cause: Database was manually dropped or setup didn’t complete

Solution:

  1. Check if users exist in the database
  2. If empty, click “Reset Setup” to run wizard again
  3. Or restore from backup

Scenario 2: Error persists after refreshing

Cause: Persistent connection issue or corrupted config

Solutions:

  1. Check your database is running
  2. Verify connection in config: config/private.ts
  3. Check logs: Look for database connection errors
  4. Last resort: Reset setup

Scenario 3: Can’t reset setup (403 Forbidden)

Cause: Security restriction - not admin and system not failed

Solutions:

  1. Wait for system to detect failure (happens automatically)
  2. Or manually authenticate as admin first
  3. Or manually delete config/private.ts via filesystem

Related Documentation

✅ Implemented Features

4 of 5 planned features now live in src/databases/database-resilience.ts and src/stores/system/state.svelte.ts.

  • ✅ Automatic retry with exponential backoff
  • ✅ Email notification to admins on database failure
  • ✅ Database connection pooling diagnostics
  • ✅ Self-healing database reconnection
  • Detailed error logs download from UI (planned)

architectureerror-handlinglogin
Was this page helpful?