# ActiveDid Migration Plan - Separate Table Architecture **Author**: Matthew Raymer **Date**: 2025-08-29T07:24Z **Status**: 🎯 **PLANNING** - Active migration planning phase ## Objective Move the `activeDid` field from the `settings` table to a dedicated `active_identity` table to improve database architecture, prevent data corruption, and separate identity selection from user preferences. ## Result This document serves as the comprehensive planning and implementation guide for the ActiveDid migration with enhanced data integrity and rollback capabilities. ## Use/Run Reference this document during implementation to ensure all migration steps are followed correctly and all stakeholders are aligned on the approach. ## Context & Scope - **In scope**: - Database schema modification for active_identity table with proper constraints - Migration of existing activeDid data with validation - Updates to PlatformServiceMixin API layer - Type definition updates - Testing across all platforms - Comprehensive rollback procedures - **Out of scope**: - Changes to user interface for identity selection - Modifications to identity creation logic - Changes to authentication flow - Updates to individual components (handled by API layer) ## Environment & Preconditions - **OS/Runtime**: All platforms (Web, Electron, iOS, Android) - **Versions/Builds**: Current development branch, SQLite database - **Services/Endpoints**: Local database, PlatformServiceMixin - **Auth mode**: Existing authentication system unchanged ## Architecture / Process Overview The migration follows a phased approach to minimize risk and ensure data integrity with enhanced validation and rollback capabilities: ```mermaid flowchart TD A[Current State
activeDid in settings] --> B[Phase 1: Schema Creation
Add active_identity table with constraints] B --> C[Phase 2: Data Migration
Copy activeDid data with validation] C --> D[Phase 3: API Updates
Update PlatformServiceMixin methods] D --> E[Phase 4: Cleanup
Remove activeDid from settings] E --> F[Final State
Separate active_identity table] G[Enhanced Rollback Plan
Schema and data rollback] --> H[Data Validation
Verify integrity at each step] H --> I[Platform Testing
Test all platforms] I --> J[Production Deployment
Gradual rollout with monitoring] K[Foreign Key Constraints
Prevent future corruption] --> L[Performance Optimization
Proper indexing] L --> M[Error Recovery
Graceful failure handling] ``` ## Interfaces & Contracts ### Database Schema Changes | Table | Current Schema | New Schema | Migration Required | |-------|----------------|------------|-------------------| | `settings` | `activeDid TEXT` | Field removed | Yes - data migration | | `active_identity` | Does not exist | New table with `activeDid TEXT` + constraints | Yes - table creation | ### Enhanced API Contract Changes | Method | Current Behavior | New Behavior | Breaking Change | |---------|------------------|--------------|-----------------| | `$accountSettings()` | Returns settings with activeDid | Returns settings with activeDid from new table | No - backward compatible | | `$saveSettings()` | Updates settings.activeDid | Updates active_identity.activeDid | Yes - requires updates | | `$updateActiveDid()` | Updates internal tracking | Updates active_identity table | Yes - requires updates | | `$getActiveIdentity()` | Does not exist | New method for active identity management | No - new functionality | ## Repro: End-to-End Procedure ### Phase 1: Enhanced Schema Creation ```sql -- Create new active_identity table with proper constraints CREATE TABLE active_identity ( id INTEGER PRIMARY KEY CHECK (id = 1), activeDid TEXT NOT NULL, lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (activeDid) REFERENCES accounts(did) ON DELETE CASCADE ); -- Add performance indexes CREATE INDEX IF NOT EXISTS idx_active_identity_activeDid ON active_identity(activeDid); CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id); -- Insert default record (will be updated during migration) INSERT INTO active_identity (id, activeDid, lastUpdated) VALUES (1, '', datetime('now')); ``` ### Phase 2: Enhanced Data Migration with Validation ```typescript // Enhanced migration script with comprehensive validation async function migrateActiveDidToSeparateTable(): Promise { const result: MigrationResult = { success: false, errors: [], warnings: [], dataMigrated: 0 }; try { // 1. Get current activeDid from settings const currentSettings = await retrieveSettingsForDefaultAccount(); const activeDid = currentSettings.activeDid; if (!activeDid) { result.warnings.push("No activeDid found in current settings"); return result; } // 2. Validate activeDid exists in accounts table const accountExists = await dbQuery( "SELECT did FROM accounts WHERE did = ?", [activeDid] ); if (!accountExists?.values?.length) { result.errors.push(`ActiveDid ${activeDid} not found in accounts table - data corruption detected`); return result; } // 3. Check if active_identity table already has data const existingActiveIdentity = await dbQuery( "SELECT activeDid FROM active_identity WHERE id = 1" ); if (existingActiveIdentity?.values?.length) { // Update existing record await dbExec( "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", [activeDid] ); } else { // Insert new record await dbExec( "INSERT INTO active_identity (id, activeDid, lastUpdated) VALUES (1, ?, datetime('now'))", [activeDid] ); } result.success = true; result.dataMigrated = 1; result.warnings.push(`Successfully migrated activeDid: ${activeDid}`); } catch (error) { result.errors.push(`Migration failed: ${error}`); logger.error("[ActiveDid Migration] Critical error during migration:", error); } return result; } // Migration result interface interface MigrationResult { success: boolean; errors: string[]; warnings: string[]; dataMigrated: number; } ``` ### Phase 3: Focused API Updates ```typescript // Updated PlatformServiceMixin method - maintains backward compatibility async $accountSettings(did?: string, defaults: Settings = {}): Promise { try { // Get settings without activeDid (unchanged logic) const settings = await this._getSettingsWithoutActiveDid(); if (!settings) { return defaults; } // Get activeDid from new table (new logic) const activeIdentity = await this._getActiveIdentity(); // Return combined result (maintains backward compatibility) return { ...settings, activeDid: activeIdentity.activeDid }; } catch (error) { logger.error("[Settings Trace] ❌ Error in $accountSettings:", error); return defaults; } } // New method for active identity management async $getActiveIdentity(): Promise<{ activeDid: string | null }> { try { const result = await this.$dbQuery( "SELECT activeDid FROM active_identity WHERE id = 1" ); if (!result?.values?.length) { return { activeDid: null }; } return { activeDid: result.values[0][0] as string }; } catch (error) { logger.error("[Settings Trace] ❌ Failed to get active identity:", error); return { activeDid: null }; } } // Enhanced method to get settings without activeDid async _getSettingsWithoutActiveDid(): Promise { const result = await this.$dbQuery( "SELECT id, accountDid, apiServer, filterFeedByNearby, filterFeedByVisible, " + "finishedOnboarding, firstName, hideRegisterPromptOnNewContact, isRegistered, " + "lastName, lastAckedOfferToUserJwtId, lastAckedOfferToUserProjectsJwtId, " + "lastNotifiedClaimId, lastViewedClaimId, notifyingNewActivityTime, " + "notifyingReminderMessage, notifyingReminderTime, partnerApiServer, " + "passkeyExpirationMinutes, profileImageUrl, searchBoxes, showContactGivesInline, " + "showGeneralAdvanced, showShortcutBvc, vapid, warnIfProdServer, warnIfTestServer, " + "webPushServer FROM settings WHERE id = ?", [MASTER_SETTINGS_KEY] ); if (!result?.values?.length) { return DEFAULT_SETTINGS; } return this._mapColumnsToValues(result.columns, result.values)[0] as Settings; } // Enhanced save settings method async $saveSettings(changes: Partial): Promise { try { // Remove fields that shouldn't be updated const { accountDid, id, activeDid, ...safeChanges } = changes; if (Object.keys(safeChanges).length > 0) { // Convert settings for database storage const convertedChanges = this._convertSettingsForStorage(safeChanges); const setParts: string[] = []; const params: unknown[] = []; Object.entries(convertedChanges).forEach(([key, value]) => { if (value !== undefined) { setParts.push(`${key} = ?`); params.push(value); } }); if (setParts.length > 0) { params.push(MASTER_SETTINGS_KEY); await this.$dbExec( `UPDATE settings SET ${setParts.join(", ")} WHERE id = ?`, params, ); } } // Handle activeDid separately in new table if (changes.activeDid !== undefined) { await this.$updateActiveDid(changes.activeDid); } return true; } catch (error) { logger.error("[PlatformServiceMixin] Error saving settings:", error); return false; } } // Enhanced update activeDid method async $updateActiveDid(newDid: string | null): Promise { try { if (newDid === null) { // Clear active identity await this.$dbExec( "UPDATE active_identity SET activeDid = '', lastUpdated = datetime('now') WHERE id = 1" ); } else { // Validate DID exists before setting const accountExists = await this.$dbQuery( "SELECT did FROM accounts WHERE did = ?", [newDid] ); if (!accountExists?.values?.length) { logger.error(`[PlatformServiceMixin] Cannot set activeDid to non-existent DID: ${newDid}`); return false; } // Update active identity await this.$dbExec( "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", [newDid] ); } // Update internal tracking await this._updateInternalActiveDid(newDid); return true; } catch (error) { logger.error("[PlatformServiceMixin] Error updating activeDid:", error); return false; } } ``` ### **Master Settings Functions Implementation Strategy** #### **1. Update `retrieveSettingsForDefaultAccount()`** ```typescript // Enhanced implementation with active_identity table integration export async function retrieveSettingsForDefaultAccount(): Promise { const platform = PlatformServiceFactory.getInstance(); // Get settings without activeDid const sql = "SELECT id, accountDid, apiServer, filterFeedByNearby, filterFeedByVisible, " + "finishedOnboarding, firstName, hideRegisterPromptOnNewContact, isRegistered, " + "lastName, lastAckedOfferToUserJwtId, lastAckedOfferToUserProjectsJwtId, " + "lastNotifiedClaimId, lastViewedClaimId, notifyingNewActivityTime, " + "notifyingReminderMessage, notifyingReminderTime, partnerApiServer, " + "passkeyExpirationMinutes, profileImageUrl, searchBoxes, showContactGivesInline, " + "showGeneralAdvanced, showShortcutBvc, vapid, warnIfProdServer, warnIfTestServer, " + "webPushServer FROM settings WHERE id = ?"; const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]); if (!result) { return DEFAULT_SETTINGS; } else { const settings = mapColumnsToValues(result.columns, result.values)[0] as Settings; // Handle JSON parsing if (settings.searchBoxes) { settings.searchBoxes = JSON.parse(settings.searchBoxes); } // Get activeDid from separate table const activeIdentityResult = await platform.dbQuery( "SELECT activeDid FROM active_identity WHERE id = 1" ); if (activeIdentityResult?.values?.length) { const activeDid = activeIdentityResult.values[0][0] as string; if (activeDid) { // Validate activeDid exists in accounts const accountExists = await platform.dbQuery( "SELECT did FROM accounts WHERE did = ?", [activeDid] ); if (accountExists?.values?.length) { settings.activeDid = activeDid; } else { logger.warn(`[databaseUtil] ActiveDid ${activeDid} not found in accounts, clearing`); // Clear corrupted activeDid await platform.dbExec( "UPDATE active_identity SET activeDid = '', lastUpdated = datetime('now') WHERE id = 1" ); } } } return settings; } } ``` #### **2. Update `$getMergedSettings()` Method** ```typescript // Enhanced implementation with active_identity table integration async $getMergedSettings(defaultKey: string, accountDid?: string, defaultFallback: Settings = {}): Promise { try { // Get default settings (now without activeDid) const defaultSettings = await this.$getSettings(defaultKey, defaultFallback); // If no account DID, return defaults with activeDid from separate table if (!accountDid) { if (defaultSettings) { // Get activeDid from separate table const activeIdentityResult = await this.$dbQuery( "SELECT activeDid FROM active_identity WHERE id = 1" ); if (activeIdentityResult?.values?.length) { const activeDid = activeIdentityResult.values[0][0] as string; if (activeDid) { // Validate activeDid exists in accounts const accountExists = await this.$dbQuery( "SELECT did FROM accounts WHERE did = ?", [activeDid] ); if (accountExists?.values?.length) { defaultSettings.activeDid = activeDid; } else { logger.warn(`[Settings Trace] ActiveDid ${activeDid} not found in accounts, clearing`); // Clear corrupted activeDid await this.$dbExec( "UPDATE active_identity SET activeDid = '', lastUpdated = datetime('now') WHERE id = 1" ); } } } } return defaultSettings || defaultFallback; } // ... rest of existing implementation for account-specific settings } catch (error) { logger.error(`[Settings Trace] ❌ Failed to get merged settings:`, { defaultKey, accountDid, error }); return defaultFallback; } } ``` ## What Works (Evidence) - ✅ **Current activeDid storage** in settings table - **Time**: 2025-08-29T07:24Z - **Evidence**: `src/db/tables/settings.ts:25` - activeDid field exists - **Verify at**: Current database schema and Settings type definition - ✅ **PlatformServiceMixin integration** with activeDid - **Time**: 2025-08-29T07:24Z - **Evidence**: `src/utils/PlatformServiceMixin.ts:108` - activeDid tracking - **Verify at**: Component usage across all platforms - ✅ **Database migration infrastructure** exists - **Time**: 2025-08-29T07:24Z - **Evidence**: `src/db-sql/migration.ts:31` - migration system in place - **Verify at**: Existing migration scripts and database versioning ## What Doesn't (Evidence & Hypotheses) - ❌ **No separate active_identity table** exists - **Time**: 2025-08-29T07:24Z - **Evidence**: Database schema only shows settings table - **Hypothesis**: Table needs to be created as part of migration - **Next probe**: Create migration script for new table - ❌ **Data corruption issues** with orphaned activeDid references - **Time**: 2025-08-29T07:24Z - **Evidence**: `IdentitySwitcherView.vue:175` - `hasCorruptedIdentity` detection - **Hypothesis**: Current schema allows activeDid to point to non-existent accounts - **Next probe**: Implement foreign key constraints in new table ## Risks, Limits, Assumptions - **Data Loss Risk**: Migration failure could lose activeDid values - **Breaking Changes**: API updates required in PlatformServiceMixin - **Rollback Complexity**: Schema changes make rollback difficult - **Testing Overhead**: All platforms must be tested with new structure - **Performance Impact**: Additional table join for activeDid retrieval - **Migration Timing**: Must be coordinated with other database changes - **Data Corruption**: Current system has documented corruption issues - **Foreign Key Constraints**: New constraints may prevent some operations ## Enhanced Rollback Strategy ### **Schema Rollback** ```sql -- If migration fails, restore original schema DROP TABLE IF EXISTS active_identity; -- Restore activeDid field to settings table if needed ALTER TABLE settings ADD COLUMN activeDid TEXT; ``` ### **Data Rollback** ```typescript // Rollback function to restore activeDid to settings table async function rollbackActiveDidMigration(): Promise { try { // Get activeDid from active_identity table const activeIdentityResult = await dbQuery( "SELECT activeDid FROM active_identity WHERE id = 1" ); if (activeIdentityResult?.values?.length) { const activeDid = activeIdentityResult.values[0][0] as string; // Restore to settings table await dbExec( "UPDATE settings SET activeDid = ? WHERE id = ?", [activeDid, MASTER_SETTINGS_KEY] ); return true; } return false; } catch (error) { logger.error("[Rollback] Failed to restore activeDid:", error); return false; } } ``` ### **Rollback Triggers** - Migration validation fails - Data integrity checks fail - Performance regression detected - User reports data loss - Cross-platform inconsistencies found ## Next Steps | Owner | Task | Exit Criteria | Target Date (UTC) | |-------|------|---------------|-------------------| | Development Team | Create enhanced migration script | Migration script with validation and rollback | 2025-08-30 | | Development Team | Update type definitions | Settings type updated, ActiveIdentity type created | 2025-08-30 | | Development Team | Update PlatformServiceMixin | Core methods updated and tested | 2025-08-31 | | Development Team | Implement foreign key constraints | Schema validation prevents corruption | 2025-08-31 | | QA Team | Platform testing | All platforms tested and verified | 2025-09-01 | | Development Team | Deploy migration | Production deployment successful | 2025-09-02 | ## References - [Database Migration Guide](./database-migration-guide.md) - [Dexie to SQLite Mapping](./dexie-to-sqlite-mapping.md) - [PlatformServiceMixin Documentation](./component-communication-guide.md) - [Migration Templates](./migration-templates/) ## Competence Hooks - *Why this works*: Separates concerns between identity selection and user preferences, prevents data corruption with foreign key constraints, centralizes identity management through API layer - *Common pitfalls*: Forgetting to implement foreign key constraints, not testing rollback scenarios, missing data validation during migration, over-engineering component updates when API layer handles everything - *Next skill unlock*: Advanced database schema design with constraints, migration planning with rollback strategies - *Teach-back*: Explain the four-phase migration approach and why each phase is necessary, especially the foreign key constraints ## Collaboration Hooks - **Sign-off checklist**: - [ ] Migration script tested on development database - [ ] Foreign key constraints implemented and tested - [ ] PlatformServiceMixin updated and tested - [ ] Rollback procedures validated - [ ] Performance impact assessed - [ ] All stakeholders approve deployment timeline ## Assumptions & Limits - Current activeDid values are valid and should be preserved - All platforms can handle the additional database table - Migration can be completed without user downtime - Rollback to previous schema is acceptable if needed - Performance impact of additional table join is minimal - Foreign key constraints will prevent future corruption - API layer updates will handle component compatibility ## What Needs to Change ### **1. Database Schema** - Create `active_identity` table with foreign key constraints - Add performance indexes - Remove `activeDid` field from `settings` table ### **2. PlatformServiceMixin Methods** - `$accountSettings()` - integrate with new table - `$saveSettings()` - handle activeDid in new table - `$updateActiveDid()` - validate and update new table - `$getActiveIdentity()` - new method for identity management ### **3. Master Settings Functions** - `retrieveSettingsForDefaultAccount()` - integrate with new table - `$getMergedSettings()` - integrate with new table ### **4. Migration Scripts** - Create migration script with validation - Implement rollback procedures - Add data corruption detection ### **5. Type Definitions** - Update Settings type to remove activeDid - Create ActiveIdentity type for new table - Update related interfaces ## What Doesn't Need to Change - **All Vue components** - API layer handles migration transparently - **Platform services** - Use PlatformServiceMixin, no direct access - **User interface** - No changes to identity selection UI - **Authentication flow** - Existing system unchanged - **Component logic** - All activeDid handling through API methods