# src/lib/db/ — SQLite Persistence Layer **Purpose**: Domain-driven SQLite persistence. Each module owns a specific table set. Schema migrations are versioned and idempotent. No raw SQL in routes — all ops go through `src/lib/db/` modules. --- ## Key Modules ### Core Infrastructure - **`core.ts`** — `getDbInstance()` returns singleton `better-sqlite3` with WAL journaling. Exports `rowToCamel()` (snake_case → camelCase), `encryptConnectionFields()` for provider credentials at rest. `SCHEMA_SQL` defines 15 base tables. - **`migrationRunner.ts`** — Applies versioned SQL files from `db/migrations/` inside transactions. Tracks applied migrations in `_omniroute_migrations`. Runs at startup; each migration is idempotent. - **`db/migrations/`** — 21 SQL files (`001_initial_schema.sql` → `021_combo_call_log_targets.sql`). Each migration has single responsibility, runs in a transaction, never fails partially. - **`localDb.ts`** — Re-export layer only. Never add logic here. Consumers import domain modules from this file for convenience. ### Domain Modules (22 total) Each module owns specific tables + CRUD operations: | Module | Tables | Responsibility | | ----------------------- | ------------------------- | ------------------------------------------------------- | | `providers.ts` | `provider_connections` | OAuth/API key provider registration and credentials | | `models.ts` | `models` | Model definitions, capabilities, pricing | | `combos.ts` | `combos`, `combo_targets` | Combo routing configs, target ordering | | `apiKeys.ts` | `api_keys` | API key lifecycle, scopes, quota tracking | | `settings.ts` | `settings` | KV store for system configuration | | `backup.ts` | Backup export/import ops | Serialize/deserialize entire DB state | | `proxies.ts` | `proxies` | MITM proxy configs and routing rules | | `prompts.ts` | `prompts` | Reusable prompt templates, versioning | | `webhooks.ts` | `webhooks` | Event-driven webhook subscriptions and logs | | `detailedLogs.ts` | `detailed_logs` | Per-request audit logging (optional, high volume) | | `domainState.ts` | `domain_state` | Transient runtime state (not persisted across restarts) | | `registeredKeys.ts` | `registered_keys` | Whitelisted API keys for MCP/A2A access | | `quotaSnapshots.ts` | `quota_snapshots` | Historical quota usage for analytics | | `modelComboMappings.ts` | `model_combo_mappings` | Map models to combo defaults | | `cliToolState.ts` | `cli_tool_state` | CLI-specific persistent state | | `encryption.ts` | — | Helpers for encrypting/decrypting sensitive fields | | `readCache.ts` | — | In-memory cache for read-heavy ops (models, providers) | | `secrets.ts` | `secrets` | Encrypted secret storage (API keys at rest) | | `stateReset.ts` | — | Wipe/reset DB state for testing or recovery | | `contextHandoffs.ts` | `context_handoffs` | Store/retrieve session context for agent handoff | | `migrations/` | — | Versioned SQL schema evolution | | `core.ts` | — | Singleton DB instance, helpers, schema definition | ### Encryption & Security - **Sensitive fields** (API keys, OAuth tokens, connection strings) encrypted at rest using `src/lib/encryption/` utilities - **`encryptConnectionFields()`** in `core.ts` — Automatic encryption when storing provider credentials - **`secrets.ts`** — Dedicated encrypted store for long-term secret handling - **Never log** SQLite encryption keys or raw secrets; always use redacted values in logs ### Testing Strategy For authoritative coverage requirements and test execution guidelines, see [`CONTRIBUTING.md#running-tests`](../../CONTRIBUTING.md#running-tests) (lines 136–162). - **Unit tests** mock `getDbInstance()` to return isolated sqlite in-memory instance - **Integration tests** use real SQLite with migrations applied, data cleaned up after each test - **No fixture interdependencies** — each test runs migrations fresh - Test files: `tests/unit/db/*.test.mjs`, `tests/integration/db/*.test.mjs` ### Anti-Patterns - ❌ Raw SQL in routes — always use domain module functions - ❌ Direct `prepare()` statements outside `db/` modules — breaks modularity - ❌ Mixing encryption logic in domain modules — use `encryption.ts` helpers only - ❌ Accessing `provider_connections` table from `combos.ts` — each module owns its tables - ❌ Skipping migrations for schema changes — all changes go through `db/migrations/` ### Adding a New Domain Module 1. Create `src/lib/db/[module].ts` with CRUD functions (create, read, update, delete, list) 2. Export from `src/lib/db/localDb.ts` (add re-export) 3. If new tables required: create migration in `db/migrations/NNN_[description].sql` 4. Run migration via `migrationRunner.ts` at startup (automatic) 5. Add unit tests in `tests/unit/db/[module].test.mjs` 6. Ensure tests meet the coverage requirements in [`CONTRIBUTING.md#running-tests`](../../CONTRIBUTING.md#running-tests) ### Performance Notes - **Read cache** (`readCache.ts`) — Pre-loads frequently accessed data (models, providers) at startup; invalidated on write - **WAL journaling** (`core.ts`) — Enables concurrent reads during writes - **Batch operations** — Use prepared statements with parameter binding to avoid SQL injection - **Connection pooling** — Singleton pattern prevents per-request connection overhead --- ## Key Decisions - **SQLite over PostgreSQL**: Simpler deployment, no separate database server, encryption at application layer - **Versioned migrations**: Each schema change is tracked, reproducible, reversible with effort - **Domain modules**: Enforces single responsibility, prevents cross-module table access - **Re-export layer**: Convenience for consumers; `localDb.ts` is re-export-only to prevent circular dependencies --- ## Review Focus - DB module changes must preserve domain boundaries (one module = one table set) - New migrations must be idempotent and run inside transactions - Encryption helpers used for all sensitive fields - Test coverage and PR requirements: see [`CONTRIBUTING.md#running-tests`](../../CONTRIBUTING.md#running-tests) (lines 136–162) - No raw SQL in routes or non-db modules