UnlockOS Developers
← Back to blog

Building Resilient State Management in Security-Critical Apps

Feb 2, 2026Feb 8, 2026
6 min
80 commits
Depth 8/10
state-managementsecuritytypescripttestingerror-handling

Building Resilient State Management in Security-Critical Applications

Introduction

In security-critical systems like smart lock management, maintaining consistent application state across browser sessions, network interruptions, and user interactions is paramount. A single state inconsistency can lead to access control failures or security vulnerabilities. This article explores patterns for building resilient state management systems that maintain security guarantees even under adverse conditions.

State Persistence and Recovery Patterns

Cross-Browser Session Synchronization

One of the most challenging aspects of web-based security systems is maintaining state consistency across multiple browser instances. Users often open multiple tabs or switch devices during a check-in process.

interface CheckinState {
  sessionId: string;
  userId: string;
  facilityId: string;
  entryKeyExpiresAt: Date;
  authenticationComplete: boolean;
}

class SecureStateManager {
  private readonly STORAGE_KEY = 'secure_checkin_state';
  
  async persistState(state: CheckinState): Promise<void> {
    // Encrypt sensitive state before storage
    const encryptedState = await this.encrypt(state);
    localStorage.setItem(this.STORAGE_KEY, encryptedState);
    
    // Broadcast state change to other tabs
    this.broadcastStateChange(state.sessionId);
  }
  
  async recoverState(): Promise<CheckinState | null> {
    const stored = localStorage.getItem(this.STORAGE_KEY);
    if (!stored) return null;
    
    try {
      const decrypted = await this.decrypt(stored);
      return this.validateStateIntegrity(decrypted);
    } catch (error) {
      // Clear corrupted state to prevent security issues
      this.clearState();
      throw new StateCorruptionError('Failed to recover state');
    }
  }
  
  private validateStateIntegrity(state: any): CheckinState {
    if (!state.sessionId || !state.facilityId) {
      throw new StateValidationError('Invalid state structure');
    }
    
    // Verify expiration times haven't been tampered with
    const expiresAt = new Date(state.entryKeyExpiresAt);
    if (expiresAt < new Date()) {
      throw new StateExpirationError('State has expired');
    }
    
    return state as CheckinState;
  }
}

Recovery Link Implementation

Infinite loops in recovery mechanisms can create denial-of-service conditions. Here's a pattern that prevents such issues:

class RecoveryLinkManager {
  private attempts = new Map<string, number>();
  private readonly MAX_ATTEMPTS = 3;
  private readonly COOLDOWN_MS = 300000; // 5 minutes
  
  async handleRecoveryAttempt(token: string, email: string): Promise<boolean> {
    const key = `${email}:${token}`;
    const attemptCount = this.attempts.get(key) || 0;
    
    if (attemptCount >= this.MAX_ATTEMPTS) {
      await this.logSecurityEvent('recovery_rate_limit', { email, token });
      throw new RateLimitError('Too many recovery attempts');
    }
    
    try {
      const isValid = await this.validateRecoveryToken(token, email);
      if (isValid) {
        this.attempts.delete(key);
        return true;
      } else {
        this.attempts.set(key, attemptCount + 1);
        this.scheduleCleanup(key);
        return false;
      }
    } catch (error) {
      await this.logSecurityEvent('recovery_validation_error', { 
        email, 
        error: error.message 
      });
      throw error;
    }
  }
  
  private scheduleCleanup(key: string): void {
    setTimeout(() => {
      this.attempts.delete(key);
    }, this.COOLDOWN_MS);
  }
}

Time-Based Security Controls

Dynamic Key Validity Management

Security keys in access control systems must have precisely controlled validity periods. Different scenarios require different expiration strategies:

interface KeyValidityConfig {
  planType: 'one-time' | 'hourly' | 'daily';
  timezone: string;
  baseValidityMinutes: number;
}

class KeyValidityManager {
  calculateKeyExpiration(config: KeyValidityConfig): Date {
    const now = new Date();
    const userTimezone = new Intl.DateTimeFormat('en', {
      timeZone: config.timezone
    }).resolvedOptions().timeZone;
    
    switch (config.planType) {
      case 'one-time':
        // Fixed 3-minute window for immediate use
        return new Date(now.getTime() + 3 * 60 * 1000);
        
      case 'hourly':
        // Valid until end of current hour in user's timezone
        return this.getEndOfHour(now, userTimezone);
        
      case 'daily':
        // Valid until end of day in user's timezone
        return this.getEndOfDay(now, userTimezone);
        
      default:
        throw new InvalidPlanTypeError(`Unknown plan type: ${config.planType}`);
    }
  }
  
  isKeyValid(expiresAt: Date): boolean {
    return new Date() < expiresAt;
  }
  
  async refreshKeyIfNeeded(sessionId: string): Promise<EntryKey | null> {
    const currentKey = await this.getCurrentKey(sessionId);
    
    if (!currentKey || !this.isKeyValid(currentKey.expiresAt)) {
      // Log key refresh for audit trail
      await this.auditLog('key_refresh', {
        sessionId,
        reason: currentKey ? 'expired' : 'missing',
        previousExpiry: currentKey?.expiresAt
      });
      
      return this.generateNewKey(sessionId);
    }
    
    return currentKey;
  }
}

UI State Synchronization with Security Constraints

User interfaces must accurately reflect the security state of the system. Here's how to implement reliable UI updates:

interface SecurityUIState {
  keyStatus: 'valid' | 'expired' | 'refreshing' | 'error';
  expiresAt: Date | null;
  canRefresh: boolean;
  lastRefresh: Date | null;
}

class SecurityUIController {
  private state: SecurityUIState = {
    keyStatus: 'expired',
    expiresAt: null,
    canRefresh: true,
    lastRefresh: null
  };
  
  async updateKeyStatus(): Promise<void> {
    try {
      this.setState({ keyStatus: 'refreshing' });
      
      const keyData = await this.keyManager.refreshKeyIfNeeded(this.sessionId);
      
      if (keyData) {
        this.setState({
          keyStatus: 'valid',
          expiresAt: keyData.expiresAt,
          lastRefresh: new Date()
        });
        
        // Schedule automatic refresh before expiration
        this.scheduleRefresh(keyData.expiresAt);
      } else {
        this.setState({ keyStatus: 'error' });
      }
    } catch (error) {
      await this.handleSecurityError(error);
      this.setState({ keyStatus: 'error' });
    }
  }
  
  private scheduleRefresh(expiresAt: Date): void {
    const refreshTime = expiresAt.getTime() - Date.now() - 30000; // 30s buffer
    
    if (refreshTime > 0) {
      setTimeout(() => {
        this.updateKeyStatus();
      }, refreshTime);
    }
  }
}

Comprehensive End-to-End Testing

Security-critical systems require exhaustive testing of state transitions and edge cases:

// Example test structure for key validity scenarios
describe('Key Validity State Machine', () => {
  const testCases = [
    {
      name: 'one-time plan key expires after 3 minutes',
      planType: 'one-time',
      expectedDurationMs: 3 * 60 * 1000,
      allowRefresh: false
    },
    {
      name: 'hourly plan key valid until end of hour',
      planType: 'hourly', 
      timezone: 'Asia/Tokyo',
      allowRefresh: true
    },
    {
      name: 'expired key shows grayscale overlay',
      planType: 'one-time',
      testExpiredUI: true
    }
  ];
  
  testCases.forEach(testCase => {
    test(testCase.name, async ({ page }) => {
      await page.goto('/checkin?plan=' + testCase.planType);
      
      // Verify initial state
      const keyElement = page.locator('[data-testid="qr-code"]');
      await expect(keyElement).toBeVisible();
      
      if (testCase.expectedDurationMs) {
        // Fast-forward time to test expiration
        await page.evaluate((ms) => {
          Date.now = () => Date.now() + ms + 1000;
        }, testCase.expectedDurationMs);
        
        await page.reload();
        
        if (testCase.testExpiredUI) {
          await expect(keyElement).toHaveClass(/expired/);
        }
      }
      
      if (testCase.allowRefresh) {
        const refreshBtn = page.locator('[data-testid="refresh-key"]');
        await expect(refreshBtn).toBeVisible();
      }
    });
  });
});

Error Handling and Recovery

Graceful Degradation Patterns

When security systems encounter errors, they must fail securely while providing clear recovery paths:

class SecureErrorHandler {
  async handleAuthenticationError(error: AuthError, context: AuthContext): Promise<RecoveryAction> {
    // Log security-relevant errors
    await this.securityLogger.log('auth_error', {
      errorType: error.constructor.name,
      userId: context.userId,
      timestamp: new Date().toISOString(),
      userAgent: context.userAgent,
      ipAddress: this.hashIP(context.ipAddress)
    });
    
    switch (error.type) {
      case 'TOKEN_EXPIRED':
        return {
          action: 'redirect_to_login',
          message: 'Your session has expired. Please log in again.',
          retryable: true
        };
        
      case 'INVALID_CREDENTIALS':
        return {
          action: 'show_error',
          message: 'Invalid credentials. Please check and try again.',
          retryable: true,
          rateLimitKey: context.userId
        };
        
      case 'ACCOUNT_LOCKED':
        return {
          action: 'show_recovery_options',
          message: 'Account temporarily locked. Use recovery email to regain access.',
          retryable: false
        };
        
      default:
        return {
          action: 'show_generic_error',
          message: 'Authentication failed. Please try again later.',
          retryable: false
        };
    }
  }
}

Summary

Building resilient state management for security-critical applications requires careful attention to:

  1. State Persistence: Implementing encrypted storage with integrity validation
  2. Cross-Session Synchronization: Coordinating state across multiple browser instances
  3. Time-Based Controls: Managing key expiration with timezone-aware calculations
  4. Recovery Mechanisms: Preventing infinite loops while maintaining security
  5. Comprehensive Testing: Covering all state transitions and edge cases
  6. Error Handling: Failing securely while providing clear recovery paths

These patterns ensure that even under adverse conditions, the system maintains its security guarantees while providing a reliable user experience. The key is to treat state management not just as a technical concern, but as a critical security control that requires the same rigor as authentication and authorization mechanisms.

Key Insights

1
Security

Encrypted State Persistence

Sensitive application state should be encrypted before storage and validated for integrity during recovery to prevent tampering.

2
Reliability

Recovery Loop Prevention

Rate limiting and cooldown periods prevent infinite recovery loops that could create denial-of-service conditions.

3
Testing

Comprehensive State Testing

Security-critical systems require exhaustive testing of all state transitions, including edge cases and error conditions.

4
Error Handling

Secure Failure Patterns

Systems should fail securely by clearing potentially corrupted state while providing clear recovery paths to users.