Login Resilience & Recovery
How SveltyCMS handles database errors and provides recovery options for users
On this page
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:
- User is authenticated as admin, OR
- System is in FAILED state
Process:
- User clicks “Reset Setup”
- Confirmation dialog appears
- System deletes
config/private.ts - Clears cached configuration
- Redirects to
/setupwizard
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:
- Check if users exist in the database
- If empty, click “Reset Setup” to run wizard again
- Or restore from backup
Scenario 2: Error persists after refreshing
Cause: Persistent connection issue or corrupted config
Solutions:
- Check your database is running
- Verify connection in config:
config/private.ts - Check logs: Look for database connection errors
- Last resort: Reset setup
Scenario 3: Can’t reset setup (403 Forbidden)
Cause: Security restriction - not admin and system not failed
Solutions:
- Wait for system to detect failure (happens automatically)
- Or manually authenticate as admin first
- Or manually delete
config/private.tsvia 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)