# config — Formation Recipe (FvW v8 §38)

**Module ID:** `config`  
**Module name:** FreshCloud Configuration Manager  
**Lifecycle phase:** formation  
**Status:** INTENT ONLY. No code yet.  
**Date:** 2026-07-21  

---

## 1. Intent (the "why")
FreshCloud requires a centralized configuration management system that bridges administrator-controlled settings with user-tier specific overrides. This module solves two critical problems: (1) providing a single source of truth for deployment-wide configurations (e.g., SMTP relay settings) and (2) enabling tier-based customization (e.g., attachment size limits) without code changes. It replaces MailSo's file-based config with PocketBase integration for persistent, auditable configuration storage while maintaining real-time performance.

## 2. User-facing behaviour
- **Administrators** can modify global settings via PocketBase admin interface
- **Users** experience tier-specific limits (e.g., free tier: 10MB attachments, pro tier: 50MB)
- **Systems** automatically reload configuration changes on API calls without downtime
- **Operators** gain observability into configuration health through metrics and audit logs

## 3. Public contract (data shapes + signatures)
```typescript
namespace FreshCloud\Mail\Config

interface ConfigManager {
  // Singleton access
  GetInstance(): ConfigManager
  Reset(): void

  // Core operations
  Load(): Promise<void>
  GetBoundaryPrefix(): string
  HasKey(key: string): bool
  Get(key: string, defaultValue?: any): any
  Set(key: string, value: any): void
  GetAll(): Map<string, any>

  // Tier-based access
  GetUserTierConfig(userTier: string): Map<string, any>
}

// Configuration types (stored in PocketBase app_settings)
type AppSettings = {
  smtp_relay_host: string
  smtp_relay_port: number
  max_attachment_size_mb: number
  message_cache_ttl_hours: number
  sieve_enabled: bool
  audit_retention_days: number
  connection_pool_max_global: number
  connection_idle_timeout_seconds: number
  max_cache_mb_per_user: number
  enable_threading: bool
  enable_labels: bool
  enable_search_attachments: bool
}

// Tier-specific overrides (stored in PocketBase users.tier field)
type TierOverrides = {
  free?: Partial<AppSettings>
  pro?: Partial<AppSettings>
  enterprise?: Partial<AppSettings>
}
```

## 4. Persistence (what gets stored where)
- **PocketBase `app_settings` table**: Stores global settings as key-value pairs (e.g., `smtp_relay_host`)
- **PocketBase `users` table**: Uses existing `tier` field to store tier-specific overrides as JSON
- **Memory**: Singleton instance holds active configuration in RAM
- **Local FS**: Generates config boundary files for security isolation
- **Audit Trail**: Uses existing `email_audit` table to log configuration changes

## 5. Dependencies (what this module uses, what uses this)
**Uses:**
- PocketBase SDK (for app_settings/users table access)
- FreshCloud logging module (for audit trails)
- FreshCloud encryption module (for sensitive config values)
- FreshCloud tier validation service (for user tier lookups)

**Used by:**
- MailSo core modules (for IMAP/SMTP configuration)
- UI bridge components (for feature flags and limits)
- Connection pooling service
- Caching service
- Audit logging service

## 6. Behaviour inventory (the N public methods)
| Method | Signature | Purpose | Side Effects | Errors |
|--------|-----------|---------|--------------|--------|
| `GetInstance` | `(): ConfigManager` | Singleton access | Returns existing instance or new one | `ConfigInitializationError` |
| `Load` | `(): Promise<void>` | Load config from PocketBase | Updates memory state; writes boundary files | `ConfigNotLoadedException` |
| `GetBoundaryPrefix` | `(): string` | Generate security prefix | None | None |
| `HasKey` | `(key: string): bool` | Check key existence | None | None |
| `Get` | `(key: string, defaultValue?: any): any` | Retrieve value with tier merge | None | `ConfigKeyNotFoundException` |
| `Set` | `(key: string, value: any): void` | Update global setting | Triggers PocketBase write; audit log | `ValidationFailedException` |
| `GetUserTierConfig` | `(userTier: string): Map<string, any>` | Get tier-specific settings | None | `InvalidTierException` |

## 7. State machine
```
[Initial] → Load() → [Loaded] → Get()/Set()/GetUserTierConfig()
               ↑             ↓
         Reset() ← ← ← [Reload requested]
```
- **Initial**: No configuration loaded
- **Loaded**: Configuration active and cached
- **Reload requested**: After PocketBase update or explicit reset
- **Transitions**:
  - `Load()` → [Loaded]
  - `Reset()` → [Initial]
  - `Get()/Set()` → No state change
  - Configuration change event → [Reload requested]

## 8. Side effects
- **Writes**:
  - Updates PocketBase `app_settings` on `Set()`
  - Writes audit log entry on config modification
  - Generates boundary files in /tmp/freshcloud/boundaries/
- **Reads**:
  - Reads `app_settings` table on `Load()`
  - Reads `users` table tier field for user lookups
- **Emits**:
  - Metrics: `config_reload_total{trigger=admin|http|boot}`
  - Events: `config.changed` with diff details

## 9. Input validation
- **Settings format**: All values must match AppSettings schema types
- **Tier names**: Must be one of `free`, `pro`, `enterprise`
- **Attachment sizes**: Must be between 1 and 100 MB
- **Hostnames**: RFC-compliant regex validation
- **Numeric ranges**: Port (1-65535), TTL (1-8760 hours)
- **Forbidden keys**: Direct modification of tier-specific keys via global `Set()`

## 10. Failure modes
| Failure | Handling | Caller Sees |
|---------|----------|-------------|
| PB connection failure | Fallback to cached config, emit metric | `ConfigServiceUnavailableException` |
| Invalid setting value | Revert last change, log validation error | `ValidationFailedException` |
| Unrecognized tier | Use `free` tier fallback, log warning | `InvalidTierException` |
| Boundary file write fail | Disable feature protections, audit security incident | `SecurityBoundaryException` |
| Singleton initialization fail | Halt application with fatal error | `FatalConfigException` |

## 11. User simulation
**Scenario 1: Admin update SMTP relay**  
Given: Admin modifies `smtp_relay_host` in PocketBase  
When: Config manager reloads  
Then: New value takes effect without restart  

**Scenario 2: Pro user upload large attachment**  
Given: User is `pro` tier (max 50MB), tries 45MB file  
When: Attachment service queries config  
Then: Upload succeeds using tier limit  

**Scenario 3: Free user attempts 25MB upload**  
Given: User is `free` tier (max 10MB), tries 25MB file  
When: Attachment service queries config  
Then: Upload rejected with size limit error  

**Scenario 4: Connection pool sizing**  
Given: Global `connection_pool_max_global` is set to 100  
When: Connection pool initializes  
Then: Creates exactly 100 connections per user session  

**Scenario 5: Audit log retention**  
Given: `audit_retention_days` is set to 90  
When: Daily cleanup job runs  
Then: Prunes email_audit records older than 90 days  

## 12. Reproducibility test
1. Install PocketBase with freshcloud schema
2. Insert initial `app_settings` test values
3. Create test user with `tier: 'free'`
4. Invoke `Config::Load()` via test script
5. Verify Get() returns correct global values
6. Test GetUserTierConfig() returns merged settings
7. Trigger configuration change in PocketBase
8. Verify Reload() updates in-memory config
9. Validate boundary file generation

## 13. Substitutability (FvW v8 §37.4)
A drop-in replacement must provide:
- Interface matching `GetBoundaryPrefix()`, `Load()`, `GetInstance()`
- Tier-based configuration merging behavior
- PocketBase integration for app_settings/users
- Boundary file generation
- Metrics emission with `config_reload_total`

## 14. Invariants (the 5 things that must always hold)
1. **Singleton guarantee**: Only one instance exists per process
2. **Tier merge priority**: User-tier overrides always override global settings
3. **Atomic boundaries**: Generated files contain only active config values
4. **PB sync state**: In-memory config never differs from PocketBase without intentional change
5. **Audit trail**: Every modification is logged to `email_audit`

## 15. Sweep fields applied (design intent)
### Sweep 1: Multi-user + PocketBase
| Field | Design Intent | PHP Implementation |
|-------|---------------|-------------------|
| user_scoping | Per-process config with user-tier filtering | Tier-specific `GetUserTierConfig()` method |
| pb_integration | Persistent admin-controlled settings | PocketBase SDK for app_settings table |
| auth_boundary | No per-user auth at config layer | Admin-only writes; reads unscoped |
| state_isolation | Single global instance | PHP singleton pattern |
| logout_behaviour | No session state | Stateless after load |
| multi_account | SMTP config shared across accounts | Global `smtp_relay_host` setting |

### Sweep 2: World-class Abilities
| Field | Design Intent | PHP Implementation |
|-------|---------------|-------------------|
| connection_pool | Pool sizing per deployment | `connection_pool_max_global` setting |
| offline_cache | Per-user cache limits | `max_cache_mb_per_user` with tier overrides |
| sieve_integration | Feature flag for UI | `sieve_enabled` boolean setting |
| audit_logging | Retention controls | `audit_retention_days` setting |
| ux_primitives | UI feature flags | `enable_threading`, `enable_labels` settings |

### Sweep 3: Migration + Observability + Tests
| Field | Design Intent | PHP Implementation |
|-------|---------------|-------------------|
| migration | One-time SnappyMail import | Separate migration service |
| observability | Config change metrics | Prometheus metrics with context tags |
| tests | Behavior validation | PHPUnit with PocketBase mock |

## 16. Cross-references
- **§11**: Module constitutional requirements (rules.md)
- **§37**: Fragment publication standards
- **§38**: Formation recipe specification
- **§19**: MailSo reference (historical extraction only)
- **Dependent modules**: MailSo core, UI bridge, connection pool, audit service

## 17. What we are intentionally NOT doing (deferred to future)
1. Hot configuration reloading via HTTP endpoint (defer to admin UI)
2. Per-user configuration storage (PB app_settings is sufficient)
3. Runtime type validation beyond schema checks
4. Configuration versioning for rollbacks
5. Multi-region configuration sync
6. Configuration history tracking beyond audit logs
7. Configuration encryption at rest (handled by PB)
8. Configuration templates (fresh deployment reset instead)
9. Configuration-dependent feature flags (handled by UI layer)
10. MailSo's 7 PHP files → simplified to 4 core methods
11. File-based configuration fallback (PB-only)
12. API documentation generation (handled by UI bridge)
13. Configuration cache invalidation strategy (built into Load())
14. MailSo's boundary generation complexity → simplified per-request generation
15. Configuration conflict resolution (admin-only write access)