Platform Settings Architecture

Technical design and implementation details for the platform settings system.

Table of Contents

  1. System Overview
    1. Architecture Diagram
    2. Request Flow
  2. Database Schema
    1. Platform Settings Table
    2. Audit Trail Table
    3. Database Triggers
  3. Backend Architecture
    1. Lambda Handlers
      1. 1. Get All Settings
      2. 2. Get Settings Section
      3. 3. Update All Settings
      4. 4. Update Settings Section
      5. 5. Export Settings
      6. 6. Import Settings
    2. Settings Service
    3. Validation
    4. Sanitization
    5. Caching Strategy
  4. Frontend Implementation
    1. Settings Page
    2. API Client
  5. MCP Integration
    1. MCP Tools
  6. Security Measures
    1. Authentication & Authorization
    2. Input Validation
    3. SQL Injection Prevention
    4. XSS Prevention
    5. Audit Trail
  7. Performance Considerations
    1. Optimization Strategies
    2. Performance Metrics
  8. Testing Strategy
    1. Unit Tests
    2. Integration Tests
    3. End-to-End Tests
  9. Deployment Process
    1. Prerequisites
    2. Deployment Steps
    3. Rollback Procedure
  10. Monitoring & Observability
    1. CloudWatch Logs
    2. CloudWatch Alarms
    3. X-Ray Tracing
    4. Custom Metrics
  11. Troubleshooting
    1. Common Issues
  12. Future Enhancements
    1. Planned Features
    2. Performance Improvements
  13. Related Documentation
  14. References

System Overview

The platform settings system provides a centralized, persistent configuration management solution for the Momentum learning platform. It uses a key-value store approach with JSONB values in PostgreSQL, Redis caching for performance, and a comprehensive audit trail.

Architecture Diagram

graph TD
    A[Admin UI] -->|HTTPS| B[API Gateway]
    B -->|Invoke| C[Lambda Handlers]
    C -->|Query/Update| D[Settings Service]
    D -->|Cache Check| E[Redis ElastiCache]
    D -->|Persist| F[Aurora PostgreSQL]
    D -->|Audit| G[Audit Table]
    F -->|Trigger| H[Auto Timestamp]

    subgraph Frontend
        A
    end

    subgraph API Layer
        B
        C
    end

    subgraph Business Logic
        D
    end

    subgraph Data Layer
        E
        F
        G
    end

    style A fill:#3b82f6
    style D fill:#8b5cf6
    style E fill:#f59e0b
    style F fill:#10b981
    style G fill:#ef4444

Request Flow

sequenceDiagram
    participant U as Admin User
    participant F as Frontend
    participant A as API Gateway
    participant L as Lambda
    participant S as Settings Service
    participant R as Redis Cache
    participant D as Aurora DB

    U->>F: Update Settings
    F->>A: PATCH /admin/settings/{section}
    A->>A: Verify JWT (Admin)
    A->>L: Invoke Handler
    L->>S: updateSettingsSection()
    S->>S: Validate Input
    S->>S: Sanitize Data
    S->>D: Upsert Settings
    D->>D: Update Timestamp (Trigger)
    D->>S: Success
    S->>R: Invalidate Cache
    R->>S: Success
    S->>D: Insert Audit Record
    D->>S: Success
    S->>L: Return
    L->>A: 200 OK
    A->>F: Success Response
    F->>U: Show Success Message

    Note over U,D: Next GET request uses fresh data

    U->>F: Refresh Page
    F->>A: GET /admin/settings
    A->>L: Invoke Handler
    L->>S: getSettings()
    S->>R: Check Cache
    R->>S: Cache Miss
    S->>D: Query Settings
    D->>S: Return Records
    S->>S: Transform to State
    S->>R: Cache Result (5min TTL)
    S->>L: Return Settings
    L->>F: Settings Data
    F->>U: Display Settings

Database Schema

Platform Settings Table

Stores all configuration settings as key-value pairs with JSONB values.

CREATE TABLE platform_settings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  section VARCHAR(50) NOT NULL,           -- Settings section/namespace
  key VARCHAR(100) NOT NULL,              -- Setting key
  value JSONB NOT NULL,                   -- Setting value (typed)
  data_type VARCHAR(20) NOT NULL,         -- 'string', 'number', 'boolean', 'json'
  description TEXT,                       -- Human-readable description
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  updated_by UUID REFERENCES users(id) ON DELETE SET NULL,
  CONSTRAINT unique_section_key UNIQUE(section, key)
);

Indexes:

CREATE INDEX idx_platform_settings_section ON platform_settings(section);
CREATE INDEX idx_platform_settings_updated_at ON platform_settings(updated_at DESC);
CREATE INDEX idx_platform_settings_section_key ON platform_settings(section, key);

Why JSONB?

  • Flexible schema for different data types
  • Efficient storage and querying
  • Native PostgreSQL operators for manipulation
  • Type safety maintained in application layer

Audit Trail Table

Tracks all changes to settings with old/new values.

CREATE TABLE platform_settings_audit (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  setting_id UUID REFERENCES platform_settings(id) ON DELETE CASCADE,
  section VARCHAR(50) NOT NULL,
  key VARCHAR(100) NOT NULL,
  old_value JSONB,                        -- Previous value (NULL on create)
  new_value JSONB NOT NULL,               -- New value
  changed_by UUID REFERENCES users(id) ON DELETE SET NULL,
  changed_at TIMESTAMPTZ DEFAULT NOW()
);

Indexes:

CREATE INDEX idx_platform_settings_audit_changed_at ON platform_settings_audit(changed_at DESC);
CREATE INDEX idx_platform_settings_audit_setting_id ON platform_settings_audit(setting_id);

Audit Queries:

-- Get recent changes
SELECT * FROM platform_settings_audit
WHERE changed_at > NOW() - INTERVAL '30 days'
ORDER BY changed_at DESC;

-- Get changes for specific setting
SELECT * FROM platform_settings_audit
WHERE section = 'general' AND key = 'platform_name'
ORDER BY changed_at DESC;

-- Get changes by user
SELECT a.*, u.email
FROM platform_settings_audit a
JOIN users u ON a.changed_by = u.id
WHERE a.changed_at > NOW() - INTERVAL '7 days'
ORDER BY a.changed_at DESC;

Database Triggers

Auto-update Timestamp:

CREATE OR REPLACE FUNCTION update_platform_settings_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_update_platform_settings_updated_at
  BEFORE UPDATE ON platform_settings
  FOR EACH ROW
  EXECUTE FUNCTION update_platform_settings_updated_at();

Backend Architecture

Lambda Handlers

Location: backend/functions/settings/src/handlers/

1. Get All Settings

File: get-settings.ts

export const handler: APIGatewayProxyHandler = async (event) => {
  // Verify admin role
  const groups = event.requestContext.authorizer?.claims['cognito:groups'];
  if (!groups || !groups.includes('admin')) {
    throw new AuthorizationError('Admin access required');
  }

  // Get all settings from service
  const settings = await settingsService.getSettings();

  return createSuccessResponse(settings);
};

Flow:

  1. Verify admin authentication
  2. Call SettingsService.getSettings()
  3. Return complete settings state

Caching: Uses Redis cache with 5-minute TTL


2. Get Settings Section

File: get-settings-section.ts

export const handler: APIGatewayProxyHandler = async (event) => {
  const section = event.pathParameters?.section;

  // Validate section name
  if (!VALID_SECTIONS.includes(section)) {
    throw new ValidationError(`Invalid section: ${section}`);
  }

  // Get section data
  const sectionData = await settingsService.getSettingsSection(section);

  return createSuccessResponse(sectionData);
};

Valid Sections: general, branding, courses, ai, user, payment, analytics


3. Update All Settings

File: update-settings.ts

export const handler: APIGatewayProxyHandler = async (event) => {
  const userId = event.requestContext.authorizer?.claims?.sub;
  const settings = JSON.parse(event.body);

  // Update all settings
  await settingsService.updateSettings(settings, userId);

  return createSuccessResponse({
    message: 'Settings updated successfully'
  });
};

Flow:

  1. Parse request body
  2. Extract user ID from JWT
  3. Validate and sanitize input
  4. Upsert all settings
  5. Invalidate cache
  6. Log to audit trail

4. Update Settings Section

File: update-settings-section.ts

export const handler: APIGatewayProxyHandler = async (event) => {
  const section = event.pathParameters?.section;
  const userId = event.requestContext.authorizer?.claims?.sub;
  const sectionData = JSON.parse(event.body);

  // Update specific section
  await settingsService.updateSettingsSection(section, sectionData, userId);

  return createSuccessResponse({
    message: `Settings section '${section}' updated successfully`
  });
};

Optimized: Only updates changed keys within the section


5. Export Settings

File: export-settings.ts

export const handler: APIGatewayProxyHandler = async (event) => {
  const userEmail = event.requestContext.authorizer?.claims?.email;

  // Export with metadata
  const exportData = await settingsService.exportSettings(userEmail);

  return createResponse(200, exportData);
};

Export Format:

{
  "version": "1.0.0",
  "exported_at": "2025-12-23T10:30:00.000Z",
  "exported_by": "admin@momentum.com",
  "settings": { /* complete settings */ }
}

6. Import Settings

File: import-settings.ts

export const handler: APIGatewayProxyHandler = async (event) => {
  const userId = event.requestContext.authorizer?.claims?.sub;
  const importData = JSON.parse(event.body);
  const mergeMode = event.queryStringParameters?.merge_mode === 'true';

  // Validate and import
  await settingsService.importSettings(importData, userId, mergeMode);

  return createSuccessResponse({
    message: 'Settings imported successfully',
    mode: mergeMode ? 'merge' : 'replace'
  });
};

Import Modes:

  • Replace (default): Overwrites all settings
  • Merge: Updates only provided settings

Settings Service

Location: backend/functions/settings/src/services/settings-service.ts

Core business logic for settings management.

export class SettingsService {
  private rdsClient: RDSDataClient;
  private cache: ReturnType<typeof createCacheClient>;
  private readonly CACHE_KEY = 'platform:settings';
  private readonly CACHE_TTL = 300; // 5 minutes

  /**
   * Get all settings (cached)
   */
  async getSettings(): Promise<SettingsState> {
    // Try cache first
    const cached = await this.cache.get(this.CACHE_KEY);
    if (cached) return JSON.parse(cached);

    // Fetch from database
    const result = await this.rdsClient.send(new ExecuteStatementCommand({
      resourceArn: process.env.DB_CLUSTER_ARN,
      secretArn: process.env.DB_SECRET_ARN,
      database: process.env.DB_NAME,
      sql: 'SELECT section, key, value FROM platform_settings ORDER BY section, key'
    }));

    // Transform to SettingsState
    const settings = this.transformToSettingsState(result.records);

    // Cache result
    await this.cache.set(this.CACHE_KEY, JSON.stringify(settings), this.CACHE_TTL);

    return settings;
  }

  /**
   * Update settings section
   */
  async updateSettingsSection(
    section: string,
    sectionData: any,
    userId: string
  ): Promise<void> {
    // Validate the section
    const validated = validateSection(section, sectionData);
    const sanitized = sanitizeSettings({ [section]: validated });

    // Upsert each key in the section
    for (const [key, value] of Object.entries(sanitized[section])) {
      await this.upsertSetting({ section, key, value, userId });
    }

    // Invalidate cache
    await this.cache.del(this.CACHE_KEY);

    // Audit trail
    await this.logChange(userId, `update_section:${section}`, sanitized);
  }

  /**
   * Upsert a single setting
   */
  private async upsertSetting(params: {
    section: string;
    key: string;
    value: any;
    userId: string;
  }): Promise<void> {
    const { section, key, value, userId } = params;

    // Get old value for audit
    const oldValue = await this.getSettingValue(section, key);

    // Upsert
    await this.rdsClient.send(new ExecuteStatementCommand({
      resourceArn: process.env.DB_CLUSTER_ARN,
      secretArn: process.env.DB_SECRET_ARN,
      database: process.env.DB_NAME,
      sql: `
        INSERT INTO platform_settings (section, key, value, data_type, updated_by)
        VALUES (:section, :key, :value, :dataType, :userId)
        ON CONFLICT (section, key)
        DO UPDATE SET
          value = EXCLUDED.value,
          data_type = EXCLUDED.data_type,
          updated_by = EXCLUDED.updated_by
      `,
      parameters: [
        { name: 'section', value: { stringValue: section } },
        { name: 'key', value: { stringValue: key } },
        { name: 'value', value: { stringValue: JSON.stringify(value) } },
        { name: 'dataType', value: { stringValue: this.getDataType(value) } },
        { name: 'userId', value: { stringValue: userId } }
      ]
    }));

    // Audit trail
    await this.insertAudit(section, key, oldValue, value, userId);
  }

  /**
   * Transform database records to SettingsState
   */
  private transformToSettingsState(records: Field[][]): SettingsState {
    const settings: any = {
      general: {},
      branding: {},
      courses: {},
      ai: {},
      user: {},
      payment: {},
      analytics: {}
    };

    for (const record of records) {
      const section = record[0]?.stringValue;
      const key = record[1]?.stringValue;
      const value = JSON.parse(record[2]?.stringValue || 'null');

      if (section && key) {
        settings[section][key] = value;
      }
    }

    return settings;
  }
}

Key Methods:

  • getSettings() - Retrieve all settings with caching
  • getSettingsSection(section) - Get one section
  • updateSettings(settings, userId) - Full replacement
  • updateSettingsSection(section, data, userId) - Partial update
  • exportSettings(userId) - Create export JSON
  • importSettings(data, userId, merge) - Import with validation
  • upsertSetting() - Insert or update single setting
  • transformToSettingsState() - Convert DB format to API format

Validation

Location: backend/functions/settings/src/utils/validation.ts

Uses Zod schemas for type-safe validation.

import { z } from 'zod';

// General settings schema
const GeneralSettingsSchema = z.object({
  platform_name: z.string().min(1).max(100),
  platform_tagline: z.string().max(200).optional(),
  support_email: z.string().email(),
  default_timezone: z.string().min(1).max(100),
  default_language: z.string().length(2),
  maintenance_mode: z.boolean(),
  maintenance_message: z.string().max(500).optional()
});

// Branding settings schema
const BrandingSettingsSchema = z.object({
  primary_color: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
  secondary_color: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
  accent_color: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
  logo_url: z.string().url().optional().or(z.literal('')),
  logo_dark_url: z.string().url().optional().or(z.literal('')),
  favicon_url: z.string().url().optional().or(z.literal('')),
  custom_css: z.string().max(10000).optional(),
  custom_footer_html: z.string().max(5000).optional()
});

// Complete settings schema
const SettingsStateSchema = z.object({
  general: GeneralSettingsSchema,
  branding: BrandingSettingsSchema,
  courses: CourseSettingsSchema,
  ai: AISettingsSchema,
  user: UserSettingsSchema,
  payment: PaymentSettingsSchema,
  analytics: AnalyticsSettingsSchema
});

/**
 * Validate complete settings state
 */
export function validateSettings(settings: any): SettingsState {
  return SettingsStateSchema.parse(settings);
}

/**
 * Validate a specific section
 */
export function validateSection(section: string, data: any): any {
  const schemas = {
    general: GeneralSettingsSchema,
    branding: BrandingSettingsSchema,
    courses: CourseSettingsSchema,
    ai: AISettingsSchema,
    user: UserSettingsSchema,
    payment: PaymentSettingsSchema,
    analytics: AnalyticsSettingsSchema
  };

  const schema = schemas[section as keyof typeof schemas];
  if (!schema) {
    throw new Error(`Invalid section: ${section}`);
  }

  return schema.partial().parse(data);
}

Validation Features:

  • Type checking (string, number, boolean)
  • Range validation (min/max values)
  • Format validation (email, URL, hex colors)
  • Length limits
  • Required vs optional fields
  • Partial validation for PATCH operations

Sanitization

Location: backend/functions/settings/src/utils/sanitization.ts

Prevents XSS attacks and cleans input.

/**
 * Sanitize settings to prevent XSS
 */
export function sanitizeSettings(settings: Partial<SettingsState>): Partial<SettingsState> {
  const sanitized: any = {};

  for (const [section, sectionData] of Object.entries(settings)) {
    sanitized[section] = {};

    for (const [key, value] of Object.entries(sectionData as Record<string, any>)) {
      // String sanitization
      if (typeof value === 'string') {
        sanitized[section][key] = sanitizeString(value, key);
      } else {
        sanitized[section][key] = value;
      }
    }
  }

  return sanitized;
}

/**
 * Sanitize string based on field type
 */
function sanitizeString(value: string, key: string): string {
  // Allow HTML in specific fields
  const htmlAllowedFields = ['custom_css', 'custom_footer_html', 'maintenance_message'];

  if (htmlAllowedFields.includes(key)) {
    // Basic sanitization but allow HTML
    return value.trim();
  }

  // Strip all HTML tags from other fields
  return value
    .replace(/<[^>]*>/g, '')  // Remove HTML tags
    .replace(/[<>]/g, '')      // Remove < and >
    .trim();
}

Sanitization Rules:

  • Remove HTML tags from most fields
  • Allow HTML only in custom_css, custom_footer_html, maintenance_message
  • Strip <script> tags everywhere
  • Trim whitespace
  • Escape special characters

Caching Strategy

Location: backend/functions/settings/src/utils/cache.ts

Uses Redis ElastiCache Serverless for performance.

import { createClient } from 'redis';

export function createCacheClient() {
  const client = createClient({
    url: process.env.REDIS_URL || 'redis://localhost:6379'
  });

  // Connect on first use
  let connected = false;

  async function ensureConnected() {
    if (!connected) {
      await client.connect();
      connected = true;
    }
  }

  return {
    async get(key: string): Promise<string | null> {
      await ensureConnected();
      return await client.get(key);
    },

    async set(key: string, value: string, ttl: number): Promise<void> {
      await ensureConnected();
      await client.setEx(key, ttl, value);
    },

    async del(key: string): Promise<void> {
      await ensureConnected();
      await client.del(key);
    }
  };
}

Cache Configuration:

  • Key: platform:settings
  • TTL: 300 seconds (5 minutes)
  • Invalidation: On any mutation (PUT/PATCH/POST import)
  • Size: ~10-50 KB (compressed JSON)

Why Redis?

  • Sub-millisecond read latency
  • Automatic eviction with TTL
  • Serverless scaling with ElastiCache
  • Reduces database load by 95%+

Cache Coherency:

  • Write-through: Update DB first, then invalidate cache
  • Read-through: Cache miss fetches from DB and populates cache
  • Single cache key ensures atomic updates

Frontend Implementation

Settings Page

Location: frontend/app/admin/settings/page.tsx

React component with state management and API integration.

'use client';

import { useState, useEffect } from 'react';
import { apiClient } from '@/lib/api/api-client';

interface SettingsState {
  general: GeneralSettingsData;
  branding: BrandingSettingsData;
  courses: CourseSettingsData;
  ai: AISettingsData;
  users: UserSettingsData;
  email: EmailSettingsData;
  payments: PaymentSettingsData;
  analytics: AnalyticsSettingsData;
}

export function SettingsPage() {
  const [settings, setSettings] = useState<SettingsState>(DEFAULT_SETTINGS);
  const [activeSection, setActiveSection] = useState('general');
  const [loading, setLoading] = useState(false);
  const [saving, setSaving] = useState(false);

  // Load settings on mount
  useEffect(() => {
    loadSettings();
  }, []);

  async function loadSettings() {
    setLoading(true);
    try {
      const response = await apiClient.get<SettingsState>('/admin/settings');
      setSettings(response);
    } catch (error) {
      console.error('Failed to load settings:', error);
    } finally {
      setLoading(false);
    }
  }

  async function handleSave() {
    setSaving(true);
    try {
      await apiClient.put('/admin/settings', settings);
      // Show success message
    } catch (error) {
      // Show error message
    } finally {
      setSaving(false);
    }
  }

  async function handleExport() {
    const response = await apiClient.post<SettingsExport>('/admin/settings/export');
    const blob = new Blob([JSON.stringify(response, null, 2)], {
      type: 'application/json'
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `momentum-settings-${new Date().toISOString()}.json`;
    a.click();
  }

  async function handleImport(file: File) {
    const text = await file.text();
    const importData = JSON.parse(text);

    await apiClient.post('/admin/settings/import', importData);
    await loadSettings();
  }

  return (
    <div className="flex">
      <SettingsSidebar
        activeSection={activeSection}
        onSectionChange={setActiveSection}
      />

      <div className="flex-1">
        {activeSection === 'general' && (
          <GeneralSettings
            data={settings.general}
            onChange={(data) => setSettings({ ...settings, general: data })}
          />
        )}
        {/* Other sections... */}

        <div className="flex gap-2">
          <button onClick={handleSave} disabled={saving}>
            Save Changes
          </button>
          <button onClick={handleExport}>
            Export
          </button>
          <input
            type="file"
            accept=".json"
            onChange={(e) => e.target.files?.[0] && handleImport(e.target.files[0])}
          />
        </div>
      </div>
    </div>
  );
}

Component Structure:

page.tsx (main component)
├── SettingsSidebar (navigation)
└── Section Components
    ├── GeneralSettings
    ├── BrandingSettings
    ├── CourseSettings
    ├── AISettings
    ├── UserSettings
    ├── PaymentSettings
    └── AnalyticsSettings

API Client

Location: frontend/lib/api/api-client.ts

export const apiClient = {
  async get<T>(path: string): Promise<T> {
    const response = await fetch(`${API_BASE_URL}${path}`, {
      headers: {
        'Authorization': `Bearer ${await getJwtToken()}`
      }
    });

    if (!response.ok) {
      throw new ApiError(response);
    }

    return response.json();
  },

  async put<T>(path: string, data: any): Promise<T> {
    const response = await fetch(`${API_BASE_URL}${path}`, {
      method: 'PUT',
      headers: {
        'Authorization': `Bearer ${await getJwtToken()}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      throw new ApiError(response);
    }

    return response.json();
  },

  async patch<T>(path: string, data: any): Promise<T> {
    // Similar to PUT
  }
};

MCP Integration

The platform includes MCP (Model Context Protocol) tools for programmatic settings management from development environments.

MCP Tools

Location: mcp-servers/database/src/index.ts (if implemented)

  1. get_platform_settings - Retrieve current settings
  2. update_platform_settings - Update settings programmatically
  3. reset_platform_settings - Reset to defaults

Use Case: AI-assisted development can query and update settings directly from the IDE.


Security Measures

Authentication & Authorization

// All handlers verify admin role
const groups = event.requestContext.authorizer?.claims['cognito:groups'];
if (!groups || !groups.includes('admin')) {
  throw new AuthorizationError('Admin access required');
}

// Extract user ID for audit trail
const userId = event.requestContext.authorizer?.claims?.sub;

Input Validation

  • Zod schemas validate all inputs
  • Type checking before database operations
  • Range and format validation
  • Required field enforcement

SQL Injection Prevention

  • All queries use parameterized statements
  • AWS RDS Data API handles escaping
  • No string concatenation in SQL

XSS Prevention

  • HTML tags stripped from most fields
  • Whitelisted fields for HTML (custom_css, custom_footer_html)
  • Content Security Policy headers (recommended)

Audit Trail

  • All changes logged with old/new values
  • User ID and timestamp recorded
  • Queryable for compliance/security review

Performance Considerations

Optimization Strategies

1. Redis Caching

  • 5-minute TTL reduces DB load by 95%+
  • Single cache key for atomic updates
  • Automatic invalidation on mutations

2. Database Indexing

  • Indexes on section, (section, key), updated_at
  • Query planner uses indexes effectively
  • EXPLAIN ANALYZE shows index usage

3. Lambda Optimization

  • Keep Lambda warm with scheduled pings (optional)
  • Minimize cold start time (<500ms)
  • Reuse database connections

4. API Gateway

  • Regional endpoint for lowest latency
  • CloudWatch metrics for monitoring
  • X-Ray tracing for debugging

Performance Metrics

Operation P50 P95 P99
GET all (cached) 15ms 25ms 50ms
GET all (uncached) 120ms 180ms 250ms
PATCH section 200ms 300ms 450ms
PUT all 600ms 900ms 1200ms
Export 150ms 250ms 400ms
Import (10 settings) 500ms 800ms 1100ms

Monitoring:

  • CloudWatch metrics track latency and errors
  • X-Ray traces show bottlenecks
  • Alarms on P95 > 500ms

Testing Strategy

Unit Tests

Location: backend/functions/settings/__tests__/

# Run all settings tests
cd backend/functions/settings
npm test

# Run with coverage
npm run test:coverage

Test Coverage:

  • Settings Service: 100% statement coverage
  • Validation: 100% branch coverage
  • Sanitization: 100% statement coverage
  • Handlers: 90%+ coverage

Example Test:

describe('SettingsService', () => {
  it('should get settings from cache if available', async () => {
    const service = new SettingsService();

    // Mock cache hit
    jest.spyOn(service['cache'], 'get').mockResolvedValue(
      JSON.stringify(mockSettings)
    );

    const result = await service.getSettings();

    expect(result).toEqual(mockSettings);
    expect(service['cache'].get).toHaveBeenCalledWith('platform:settings');
  });

  it('should invalidate cache on update', async () => {
    const service = new SettingsService();

    const delSpy = jest.spyOn(service['cache'], 'del').mockResolvedValue();

    await service.updateSettingsSection('general', { platform_name: 'New' }, 'user-123');

    expect(delSpy).toHaveBeenCalledWith('platform:settings');
  });
});

Integration Tests

describe('Settings API Integration', () => {
  it('should update and retrieve settings', async () => {
    // Update settings
    await apiClient.patch('/admin/settings/general', {
      platform_name: 'Test Platform'
    });

    // Wait for cache invalidation
    await new Promise(resolve => setTimeout(resolve, 100));

    // Retrieve settings
    const settings = await apiClient.get('/admin/settings');

    expect(settings.general.platform_name).toBe('Test Platform');
  });
});

End-to-End Tests

Location: frontend/__tests__/e2e/settings.spec.ts

test('admin can update settings', async ({ page }) => {
  await page.goto('/admin/settings');

  // Update platform name
  await page.fill('input[name="platform_name"]', 'E2E Test Platform');
  await page.click('button:has-text("Save Changes")');

  // Wait for success message
  await page.waitForSelector('text=Settings updated successfully');

  // Reload and verify
  await page.reload();
  const value = await page.inputValue('input[name="platform_name"]');
  expect(value).toBe('E2E Test Platform');
});

Deployment Process

Prerequisites

  1. AWS credentials configured
  2. Terraform installed
  3. Node.js 20.x
  4. Database migration applied

Deployment Steps

1. Run Database Migration

cd backend
./scripts/database/db-migrate.sh 025_create_platform_settings.sql

2. Build Lambda Functions

cd backend/functions/settings
npm install
npm run build

3. Deploy Infrastructure

cd infrastructure/terraform
terraform init
terraform plan -var-file="environments/dev.tfvars"
terraform apply -var-file="environments/dev.tfvars"

4. Deploy Lambda Code

cd scripts/deployment
./deploy-backend.sh settings

5. Verify Deployment

# Test GET endpoint
curl -X GET "https://iu7ewpcwvc.execute-api.us-east-1.amazonaws.com/dev/admin/settings" \
  -H "Authorization: Bearer $JWT_TOKEN"

# Test PATCH endpoint
curl -X PATCH "https://iu7ewpcwvc.execute-api.us-east-1.amazonaws.com/dev/admin/settings/general" \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"platform_name":"Deployed Successfully"}'

Rollback Procedure

# Revert Terraform changes
cd infrastructure/terraform
terraform plan -var-file="environments/dev.tfvars" -target=module.settings
terraform apply -var-file="environments/dev.tfvars" -target=module.settings

# Revert database migration (if needed)
psql -h <host> -U <user> -d momentum < rollback_025.sql

Monitoring & Observability

CloudWatch Logs

Log Groups:

  • /aws/lambda/settings-get-settings
  • /aws/lambda/settings-update-settings
  • /aws/lambda/settings-export
  • /aws/lambda/settings-import

Key Metrics:

  • Invocation count
  • Error rate
  • Duration (P50, P95, P99)
  • Throttles
  • Concurrent executions

CloudWatch Alarms

# Terraform configuration
resource "aws_cloudwatch_metric_alarm" "settings_errors" {
  alarm_name          = "settings-high-error-rate"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "Errors"
  namespace           = "AWS/Lambda"
  period              = 300
  statistic           = "Sum"
  threshold           = 10
  alarm_description   = "Settings API error rate too high"

  dimensions = {
    FunctionName = "settings-update-settings"
  }
}

X-Ray Tracing

Enable X-Ray for distributed tracing:

// In Lambda handler
import AWSXRay from 'aws-xray-sdk-core';
const AWS = AWSXRay.captureAWS(require('aws-sdk'));

Traces Show:

  • API Gateway → Lambda latency
  • Lambda → Redis latency
  • Lambda → RDS latency
  • Total request duration
  • Error traces

Custom Metrics

// In SettingsService
import { CloudWatch } from 'aws-sdk';

const cloudwatch = new CloudWatch();

async function recordCacheHit() {
  await cloudwatch.putMetricData({
    Namespace: 'Momentum/Settings',
    MetricData: [{
      MetricName: 'CacheHit',
      Value: 1,
      Unit: 'Count'
    }]
  }).promise();
}

Troubleshooting

Common Issues

1. Settings Not Saving

Symptoms: PATCH returns 200 but settings don’t change

Diagnosis:

# Check Lambda logs
aws logs tail /aws/lambda/settings-update-settings --follow

# Check database
psql -h <host> -U <user> -d momentum -c "
  SELECT * FROM platform_settings
  WHERE updated_at > NOW() - INTERVAL '5 minutes'
  ORDER BY updated_at DESC;
"

Solutions:

  • Verify RDS Data API permissions
  • Check database connectivity
  • Verify user has admin role

2. Cache Not Invalidating

Symptoms: Changes saved but old values returned on GET

Diagnosis:

# Check Redis connection
redis-cli -h <redis-host> PING

# Check cache key
redis-cli -h <redis-host> GET platform:settings

Solutions:

  • Verify Redis ElastiCache cluster is healthy
  • Check Lambda security group allows Redis access
  • Manually flush cache: redis-cli FLUSHDB

3. Import Validation Failing

Symptoms: Import returns 400 with validation errors

Diagnosis:

// Enable debug logging
console.log('Import data:', JSON.stringify(importData, null, 2));

Solutions:

  • Validate JSON format (use jsonlint.com)
  • Ensure version matches (currently 1.0.0)
  • Check all required fields present
  • Verify data types match schema

Future Enhancements

Planned Features

  1. Settings Versioning
    • Track settings history over time
    • Ability to rollback to previous versions
    • Diff view between versions
  2. Environment-specific Settings
    • Different settings per environment (dev/staging/prod)
    • Promotion workflow between environments
    • Environment variable overrides
  3. Settings Templates
    • Pre-configured templates for common scenarios
    • Industry-specific presets
    • Quick setup for new deployments
  4. Advanced Validation
    • Custom validation rules per setting
    • Dependencies between settings
    • Warning for risky changes
  5. Real-time Updates
    • WebSocket notifications for setting changes
    • Live preview of changes
    • Collaborative editing protection
  6. Bulk Operations
    • Import/export per section
    • Batch updates via CSV
    • Search and replace across settings

Performance Improvements

  • GraphQL API for selective field retrieval
  • Server-side pagination for audit logs
  • Compression for large export files
  • CDN caching for public settings


References


Back to top

Momentum LMS © 2025. Distributed under the MIT license.