# cache — Formation Recipe (FvW v8 §38)

**Module ID:** `cache`  
**Module name:** Cache Service  
**Lifecycle phase:** formation  
**Status:** INTENT ONLY. No code yet.  
**Date:** 2026-07-21  

---

## 1. Intent (the "why")
FreshCloud needs a performance-optimized caching layer that enables offline access, reduces mail server load, and maintains strict multi-user isolation. This module provides a unified abstraction for time-sensitive data storage while enabling auditing, quota management, and clean migration from legacy systems. The cache solves three core problems: (1) Enables offline access to emails/settings (2) Reduces repeated expensive IMAP/SMTP calls (3) Enforces data boundaries in multi-tenant environments.

---

## 2. User-facing behaviour
- **get** – Retrieve cached data by key for authenticated user  
- **set** – Store new data with expiration time for authenticated user  
- **delete** – Remove specific cached entry for authenticated user  
- **clearNamespace** – Purge all cache entries for user/account pair  
- **exists** – Check if a key exists in user's cache  
- **stats** – Retrieve hit/miss rates, size, and eviction metrics  

---

## 3. Public contract (data shapes + signatures)
```typescript
// FreshCloud\Mail\Cache\CacheClient
interface CacheClient {
  get(key: string): Promise<mixed>;
  set(key: string, value: mixed, ttl: int): Promise<boolean>;
  delete(key: string): Promise<boolean>;
  clearNamespace(userId: string, accountId: string): Promise<boolean>;
  exists(key: string): Promise<boolean>;
  stats(): Promise<{
    hitRate: float,
    missRate: float,
    sizeBytes: int,
    lastEviction: DateTime,
    evictionsTotal: int
  }>;
}

// FreshCloud\Mail\Cache\DriverInterface
interface DriverInterface {
  get(key: string): mixed;
  set(key: string, value: mixed, ttl: int): boolean;
  delete(key: string): boolean;
  clearNamespace(userId: string, accountId: string): boolean;
  encrypt(value: mixed, userId: string): string;
  decrypt(ciphertext: string, userId: string): mixed;
}

// FreshCloud\Mail\Cache\Drivers\File
class File implements DriverInterface {
  // File-based driver with encryption
}
```

---

## 4. Persistence (what gets stored where)
- **Local Storage**:  
  - Encrypted cache files at `data/cache/users/{user_id}/{account_id}/{sha256_key}.dat`  
  - Per-user cache directory structure  
  - Format: `IV (16B) + HMAC (32B) + AES-GCM ciphertext`  

- **PocketBase Tables**:  
  - `email_cache_index` (new schema):  
    - `key` (string, primary)  
    - `user_id` (string, indexed)  
    - `account_id` (string, indexed)  
    - `size_bytes` (int)  
    - `last_accessed` (datetime)  
    - `expires_at` (datetime, indexed)  
  - Reuse `email_audit` table for cache audit events  

---

## 5. Dependencies (what this module uses, what uses this)
- **Uses**:  
  - FreshCloud Auth (for user_id context)  
  - FreshCloud Log (for audit events)  
  - FreshCloud Encryption (AES-GCM ops)  
  - PocketBase (metadata persistence)  
  - Symfony Filesystem (PHP operations)  

- **Used by**:  
  - Mail Engine (IMAP/SMTP responses)  
  - Contact Manager (cached address books)  
  - Settings Service (user preferences)  
  - Composer (package.json)  

---

## 6. Behaviour inventory (the N public methods)
| Method | Signature | Purpose | Side Effects | Errors |
|--------|-----------|---------|--------------|---------|
| `get` | `get(string key): mixed` | Retrieve cached value | Updates `last_accessed` in PB | `KeyNotFound`, `DecryptionFailed` |
| `set` | `set(string key, mixed value, int ttl): bool` | Store encrypted value | Creates cache file + PB index | `QuotaExceeded`, `EncryptionFailed` |
| `delete` | `delete(string key): bool` | Remove cached entry | Deletes file + PB row | `KeyNotFound` |
| `clearNamespace` | `clearNamespace(string userId, string accountId): bool` | Purge all user entries | Deletes all files + PB rows | `PermissionDenied` |
| `exists` | `exists(string key): bool` | Key presence check | Updates PB `last_accessed` | `KeyNotFound` |
| `stats` | `stats(): object` | Cache performance data | None | `QuotaExceeded` |

---

## 7. State machine
| State | Triggers | Exit Conditions |
|-------|----------|----------------|
| IDLE | Initial state / Cleanup complete | `get/set/delete` called |
| CACHING | `set` invoked | Cache file written + PB index created |
| READING | `get/exists` invoked | Value retrieved or missing |
| DELETING | `delete` called | File + PB entry removed |
| CLEARING | `clearNamespace` called | All files + PB entries removed |
| EXPIRING | TTL elapses | Auto-delete triggered |

---

## 8. Side effects
- **Write Operations**:  
  - Filesystem: Create/update/delete cache files  
  - PocketBase: Insert/update/delete `email_cache_index` rows  
  - Audit Log: `CACHE_WRITE`, `CACHE_DELETE`, `CACHE_EXPIRE` events at `TRACE` level  

- **Read Operations**:  
  - Filesystem: Read cache files  
  - PocketBase: Query `email_cache_index` for metadata  

- **Emit Operations**:  
  - Audit events: `CACHE_HIT`, `CACHE_MISS`, `CACHE_EVICTION`  
  - Metrics: `cache_hits_total`, `cache_misses_total`, `cache_size_bytes`  

---

## 9. Input validation
- **key**: Must match `^[a-zA-Z0-9_-]{1,64}$`  
- **value**: Must be JSON-serializable (max 1MB)  
- **ttl**: Integer ≥ 0 (seconds)  
- **userId**: Non-empty string (format: UUID)  
- **accountId**: Non-empty string (format: UUID)  
- **Namespace**: Must be `user_{userId}_{accountId}`  

---

## 10. Failure modes
| Failure | Handling | Visibility |
|---------|----------|------------|
| **Filesystem I/O** | Throw `CacheException` | Log to email_audit (ERROR) |
| **Quota exceeded** | Return `false` from `set` | Log quota violation (WARN) |
| **Decryption failure** | Throw `SecurityException` | Log security event (CRITICAL) |
| **TTL expiration** | Auto-delete during `get` | Audit `CACHE_EXPIRE` event |
| **Invalid key** | Throw `InvalidArgumentException` | Log validation error (TRACE) |

---

## 11. User simulation (test scenarios)
1. **Cache Miss**  
   Given: New user  
   When: Call `get("inbox")`  
   Then: Returns `null` and emits `CACHE_MISS`  

2. **Cache Hit**  
   Given: Mail body cached  
   When: Call `get("msg_123")`  
   Then: Returns MIME data and emits `CACHE_HIT`  

3. **TTL Expiry**  
   Given: Set with 60s TTL  
   When: Wait 61s then `get("temp_data")`  
   Then: Returns `null` and auto-deletes  

4. **Namespace Isolation**  
   Given: Two users (U1, U2)  
   When: U1 sets "contact_list", U2 gets "contact_list"  
   Then: U2 returns `null`  

5. **Logout Cleanup**  
   Given: Active cache for user  
   When: Logout sequence  
   Then: All namespace files + PB rows deleted  

6. **Quota Enforcement**  
   Given: User at 90% quota  
   When: Call `set("large_file", 2MB)`  
   Then: Returns `false` and logs quota warning  

---

## 12. Reproducibility test
1. Create `data/cache/users/{user_id}/{account_id}` directories  
2. Initialize `email_cache_index` PocketBase table  
3. Implement AES-GCM encryption with `userId`-derived key  
4. Write `get/set/delete/clearNamespace` methods with PB integration  
5. Implement `stats()` with hit/miss calculation  
6. Add audit logging for all cache operations  
7. Create unit tests for TTL and namespace isolation  
8. Add integration tests with real filesystem + PocketBase  
9. Benchmark cache operations against 1000+ keys  

---

## 13. Substitutability (FvW v8 §37.4)
Alternative implementations MUST provide:  
- Namespace-scoped operations (`get/set/delete/clearNamespace`)  
- User-derived encryption (AES-GCM)  
- PocketBase metadata integration  
- Audit event emission (`CACHE_HIT`, `CACHE_MISS`, etc.)  
- Same public method signatures and error types  
- Stats interface with identical return structure  

---

## 14. Invariants (the 5 things that must always hold)
1. **Zero Data Cross-Contamination**: User A cannot access User B's cache entries  
2. **Tamper Evidence**: All stored data encrypted with `userId`-derived keys  
3. **Quota Enforcement**: `set()` rejects writes when quota exceeded  
4. **TTL Enforcement**: Expired auto-deleted during `get()`/`exists()` calls  
5. **Audit Completeness**: All cache operations logged to `email_audit`  

---

## 15. Sweep fields applied (the 14 design intent fields)
### Sweep 1: Multi-user + PocketBase (6 fields)
- **user_scoping**  
  Sweep: Multi-user  
  Design: Namespacing via `user_{userId}_{accountId}`  
  PHP: Added `namespace` parameter to all methods  

- **pb_integration**  
  Sweep: Multi-user  
  Design: `email_cache_index` table for metadata  
  PHP: PB client in `Drivers\File::storeMetadata()`  

- **auth_boundary**  
  Sweep: Multi-user  
  Design: No auth checks (relying on auth module context)  
  PHP: No auth code in cache module  

- **state_isolation**  
  Sweep: Multi-user  
  Design: Hard isolation by namespace directories  
  PHP: `{userId}/{accountId}` path segments  

- **logout_behaviour**  
  Sweep: Multi-user  
  Design: Delete all user entries on logout  
  PHP: `clearNamespace()` called by auth service  

- **multi_account**  
  Sweep: Multi-user  
  Design: Separate namespace per account  
  PHP: `accountId` in all operations  

### Sweep 2: World-class Abilities (5 fields)
- **offline_cache**  
  Sweep: World-class Abilities  
  Design: First-class offline support  
  PHP: `get()` returns data without network calls  

- **sieve_integration**  
  Sweep: World-class Abilities  
  Design: Sieve scripts cached with short TTL  
  PHP: Dedicated `sieve_{hash}` namespace  

- **audit_logging**  
  Sweep: World-class Abilities  
  Design: Comprehensive audit at TRACE level  
  PHP: `Log::trace()` for all cache operations  

- **ux_primitives**  
  Sweep: World-class Abilities  
  Design: Admin interface exposure  
  PHP: `stats()` method returns all needed metrics  

- **connection_pool**  
  Sweep: Not applicable (file-based)  

### Sweep 3: Migration + Observability + Tests (3 fields)
- **migration**  
  Sweep: Migration  
  Design: SnappyMail to FreshCloud migration  
  PHP: `Migrator` class (deferred to v2)  

- **observability**  
  Sweep: Observability  
  Design: Metrics + alerts  
  PHP: `Metrics::counter()` for all operations  

- **tests**  
  Sweep: Tests  
  Design: PHPUnit coverage for all contracts  
  PHP: 20+ unit tests in `tests/cache/*`  

---

## 16. Cross-references
- §11: Contract primacy (recipe + codex as source of truth)  
- §19: Recipe extraction from historical reference (MailSo)  
- §37: Substitutability requirements  
- §38: Formation phase documentation  
- Dependent modules: Auth, Log, Encryption, PocketBase  

---

## 17. What we are intentionally NOT doing (deferred to future)
1. Redis driver (v2: optional Redis overlay)  
2. TTL cleanup daemon (v1: lazy cleanup)  
3. Cache warming at login (v1: on-demand)  
4. Memory-based caching (v1: file-only)  
5. Sharding across disks (v1: single storage tier)  
6. Distributed caching (v1: per-node)  
7. Adaptive TTL based on access patterns (v2: machine learning)  
8. Cache compression (v1: encryption covers size reduction)  
9. Partial data retrieval (v1: atomic cache units)  
10. Re-use of MailSo's `CacheDriver` hierarchy (clean-room new design)  
11. MailSo's 7 PHP files → 4 files (removed `CacheManager`, `CacheManagerException`, `Config` for simplicity)  
12. AGPLv3 driver support (MIT from day one)  
13. Cache quota tracking in UI (v2: Admin panel)  
14. Secondary backup storage (v1: single disk assumed)