--- status: stable last_updated: 2026-04-22 --- # Storage Spec Phase 1 Resolutions Architectural decisions made during the storage spec stabilization planning session on 2026-04-22. These resolutions inform all downstream task execution. ## Decisions ### D1. Cascade Policy Defaults | Data Category | Default Cascade | Rationale | |---|---|---| | Audit/traceability data | RESTRICT on NOT NULL FKs; SET NULL on nullable FKs | NOT NULL FKs (ownerId) prevent account deletion. Nullable FKs (keyId, sessionId, orgId) preserve the row while clearing the reference. Both patterns prevent data loss. | | Live session data | Nullable FK + SET NULL | Orphaned sessions preserve conversation history for audit/debugging | | Ephemeral config (spoke ops, etc.) | CASCADE | Delete with parent — these are runtime artifacts | | Transferable ownership | RESTRICT + transfer workflow | Cannot delete account that owns an org; must transfer first | ### D2. Message IDs — Composite Index Approach **Decision**: Messages table keeps UUIDv4 (`commonCols.id`). Ordering is handled by composite index `(session_id, created_at, id)`. **Rationale**: - ADR-003's sortable IDs remain in effect for `parts` only - Composite index provides efficient ordering for messages without requiring sortable IDs - Simpler opencode conversation import — opencode uses UUIDv4 message IDs natively - ADR-003 is amended to scope sortable IDs to `parts`, not `messages` **Action**: Amend ADR-003, update sessions.md, update table-reference.md ### D3. Operations Schema — Definitions + Registrations Split (Option A) **Decision**: Split `operation_specs` into two tables: 1. **`operations`** (definitions): `id`, `namespace`, `name`, `type` (query/mutation/subscription), `inputSchema`, `outputSchema`, `accessControl`, `description` 2. **`operation_registrations`**: `id`, `operationId → operations.id`, `providerType` (spoke|client), `providerId`, `status` (active|inactive), `registeredAt` **Rationale**: - Separation of "what an operation is" from "who provides it right now" - Multiple instances of the same client (e.g., 5 opencode instances) share definitions but have separate registrations - OpenAPI/MCP spec imports create definitions; spoke/client connection creates registrations - On spoke disconnect: registration rows are deactivated (not deleted). Definitions survive. - On admin spoke-row deletion: registrations CASCADE (ephemeral config pattern from D1) - Call routing: resolve from definition → active registrations → provider - More upfront schema work, but avoids a confusing refactor later when multi-instance clients arrive **Namespace convention**: `operations.namespace`/`name` store **post-remap** identifiers (e.g., `dev.{spokeId}.fs.read`). This ensures uniqueness across multiple providers of the same logical operation. Pre-remap identifiers are stored in `operation_registrations.metadata` for traceability. **Actions**: - Rename `operation_specs` → `operations` across all docs - Add `operation_registrations` table spec to spokes.md - Update table-reference.md with new FK relationships and cascade policies - Update spokes.md disconnect lifecycle to deactivate registrations, not delete - Update ADR-006 to reflect the split ### D4. Key Rotation - **API key rotation**: Handled by keypal library (ADR-004) - **Client secret encryption**: Needs multi-key format specification. Current `HUB_ENCRYPTION_KEY` (singular, env var) was insufficient — superseded by the two-layer key model in [hub-config.md](../architecture/hub-config.md) and ADR-008 (revised). Task `specify-key-rotation-protocol` addresses this. ### D5. Account Deactivation **Decision**: Add `status` enum column (`active` | `suspended` | `deactivated`) to accounts table, not a boolean. **Rationale**: More extensible — allows distinguishing "admin suspended" from "user deactivated" in the future. Consistent with having meaningful status semantics rather than overloaded booleans. **Action**: Update identity.md accounts table spec, update table-reference.md ### D6. System Account Email Convention **Decision**: Email reservation for system/LLM accounts is **deployment-configurable**, not hardcoded to any domain. **Convention**: Deployments MAY reserve an email domain or pattern (e.g., `{model}@llm.example.com` or `{model}@system.example.com`) for non-human accounts. This prevents collision between human and system-generated accounts and enables attribution in git and audit logs. **Anti-pattern**: Do NOT hardcode any specific domain (e.g., `alk.dev`) in architecture documentation. The convention is generic; the specific domain is a deployment concern. **Action**: Update identity.md to document the configurable pattern convention, not a specific domain. ## References - docs/reviews/storage-architecture-review-2026-04-21.md — source review - tasks/architecture/storage/* — downstream implementation tasks