You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

28 KiB

ActiveDid Migration Plan - Separate Table Architecture

Author: Matthew Raymer Date: 2025-01-27T18:30Z 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 and separate identity selection from user preferences.

Result

This document serves as the comprehensive planning and implementation guide for the ActiveDid migration.

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
    • Migration of existing activeDid data
    • Updates to all platform services and mixins
    • Type definition updates
    • Testing across all platforms
  • Out of scope:
    • Changes to user interface for identity selection
    • Modifications to identity creation logic
    • Changes to authentication flow

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:

flowchart TD
    A[Current State<br/>activeDid in settings] --> B[Phase 1: Schema Creation<br/>Add active_identity table]
    B --> C[Phase 2: Data Migration<br/>Copy activeDid data]
    C --> D[Phase 3: API Updates<br/>Update all access methods]
    D --> E[Phase 4: Cleanup<br/>Remove activeDid from settings]
    E --> F[Final State<br/>Separate active_identity table]

    G[Rollback Plan<br/>Keep old field until verified] --> H[Data Validation<br/>Verify integrity at each step]
    H --> I[Platform Testing<br/>Test all platforms]
    I --> J[Production Deployment<br/>Gradual rollout]

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 Yes - table creation

API Contract Changes

Method Current Behavior New Behavior Breaking Change
$accountSettings() Returns settings with activeDid Returns settings without activeDid 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

Repro: End-to-End Procedure

Phase 1: Schema Creation

-- Create new active_identity table
CREATE TABLE active_identity (
  id INTEGER PRIMARY KEY CHECK (id = 1),
  activeDid TEXT NOT NULL,
  lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Insert default record (will be updated during migration)
INSERT INTO active_identity (id, activeDid) VALUES (1, '');

Phase 2: Data Migration

// Migration script to copy existing activeDid values
async function migrateActiveDidToSeparateTable(): Promise<void> {
  // Get current activeDid from settings
  const currentSettings = await retrieveSettingsForDefaultAccount();
  const activeDid = currentSettings.activeDid;

  if (activeDid) {
    // Insert into new table
    await dbExec(
      "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
      [activeDid]
    );
  }
}

Phase 3: API Updates

// Updated PlatformServiceMixin method
async $accountSettings(did?: string, defaults: Settings = {}): Promise<Settings> {
  // Get settings without activeDid
  const settings = await this._getSettingsWithoutActiveDid();

  // Get activeDid from separate table
  const activeIdentity = await this._getActiveIdentity();

  return { ...settings, activeDid: activeIdentity.activeDid };
}

// New method to get settings without activeDid
async _getSettingsWithoutActiveDid(): Promise<Settings> {
  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;
}

// New method to get active identity
async _getActiveIdentity(): Promise<{ activeDid: string | null }> {
  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 };
}

Master Settings Functions Implementation Strategy

1. Update retrieveSettingsForDefaultAccount()

// Current implementation in src/db/databaseUtil.ts:148
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
  const platform = PlatformServiceFactory.getInstance();
  const sql = "SELECT * FROM settings WHERE id = ?";
  const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]);
  // ... rest of implementation
}

// Updated implementation
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
  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) {
      settings.activeDid = activeIdentityResult.values[0][0] as string;
    }

    return settings;
  }
}

2. Update $getMergedSettings() Method

// Current implementation in PlatformServiceMixin.ts:485
async $getMergedSettings(defaultKey: string, accountDid?: string, defaultFallback: Settings = {}): Promise<Settings> {
  // Get default settings
  const defaultSettings = await this.$getSettings(defaultKey, defaultFallback);
  // ... rest of implementation
}

// Updated implementation
async $getMergedSettings(defaultKey: string, accountDid?: string, defaultFallback: Settings = {}): Promise<Settings> {
  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) {
          defaultSettings.activeDid = activeIdentityResult.values[0][0] as string;
        }
      }
      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-01-27T18:30Z
    • 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-01-27T18:30Z
    • Evidence: src/utils/PlatformServiceMixin.ts:108 - activeDid tracking
    • Verify at: Component usage across all platforms
  • Database migration infrastructure exists

    • Time: 2025-01-27T18:30Z
    • Evidence: src/db-sql/migration.ts:31 - migration system in place
    • Verify at: Existing migration scripts and database versioning
  • Master settings functions architecture supports migration

    • Time: 2025-01-27T18:30Z
    • Evidence: Functions use explicit field selection, not SELECT *
    • Verify at: src/db/databaseUtil.ts:148 and src/utils/PlatformServiceMixin.ts:442

What Doesn't (Evidence & Hypotheses)

  • No separate active_identity table exists

    • Time: 2025-01-27T18:30Z
    • 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
  • Platform services hardcoded to settings table

    • Time: 2025-01-27T18:30Z
    • Evidence: src/services/platforms/*.ts - direct settings table access
    • Hypothesis: All platform services need updates
    • Next probe: Audit all platform service files for activeDid usage

Risks, Limits, Assumptions

  • Data Loss Risk: Migration failure could lose activeDid values
  • Breaking Changes: API updates required across all platform services
  • 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

Next Steps

Owner Task Exit Criteria Target Date (UTC)
Development Team Create migration script Migration script tested and validated 2025-01-28
Development Team Update type definitions Settings type updated, ActiveIdentity type created 2025-01-28
Development Team Update platform services All services use new active_identity table 2025-01-29
Development Team Update PlatformServiceMixin Mixin methods updated and tested 2025-01-29
QA Team Platform testing All platforms tested and verified 2025-01-30
Development Team Deploy migration Production deployment successful 2025-01-31

References

Competence Hooks

  • Why this works: Separates concerns between identity selection and user preferences, improves database normalization, enables future identity management features
  • Common pitfalls: Forgetting to update all platform services, not testing rollback scenarios, missing data validation during migration
  • Next skill unlock: Advanced database schema design and migration planning
  • Teach-back: Explain the four-phase migration approach and why each phase is necessary

Collaboration Hooks

  • Sign-off checklist:
    • Migration script tested on development database
    • All platform services updated and tested
    • Rollback plan 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

Component & View Impact Analysis

High Impact Components

  1. IdentitySection.vue - Receives activeDid as prop

    • Current: Uses activeDid from parent component via prop
    • Impact: NO CHANGES REQUIRED - Parent component handles migration
    • Risk: LOW - No direct database access

    Current Implementation:

    <template>
      <div class="text-sm text-slate-500">
        <div class="font-bold">ID:&nbsp;</div>
        <code class="truncate">{{ activeDid }}</code>
        <!-- ... rest of template -->
      </div>
    </template>
    
    <script lang="ts">
    export default class IdentitySection extends Vue {
      @Prop({ required: true }) activeDid!: string; // Received as prop from parent
      // ... other props and methods
    }
    </script>
    

    Required Changes:

    <script lang="ts">
    export default class IdentitySection extends Vue {
      @Prop({ required: true }) activeDid!: string; // No changes needed
      // ... other props and methods
    }
    </script>
    

    Key Insight: This component requires zero changes since it receives activeDid as a prop. The parent component that provides this prop will handle the migration automatically through the API layer updates.

  2. DIDView.vue - Heavy activeDid usage

    • Current: Initializes activeDid in mounted() lifecycle
    • Impact: Must update initialization logic to use new table
    • Risk: HIGH - Primary DID viewing component

    Current Implementation:

    <script lang="ts">
    export default class DIDView extends Vue {
      activeDid = ""; // Component data
    
      async mounted() {
        await this.initializeSettings();
        await this.determineDIDToDisplay();
        // ... rest of initialization
      }
    
      private async initializeSettings() {
        const settings = await this.$accountSettings();
        this.activeDid = settings.activeDid || "";
        this.apiServer = settings.apiServer || "";
      }
    
      private async determineDIDToDisplay() {
        const pathParam = window.location.pathname.substring("/did/".length);
        let showDid = pathParam;
    
        if (!showDid) {
          // No DID provided in URL, use active DID
          showDid = this.activeDid; // Uses component data
          this.notifyDefaultToActiveDID();
        }
        // ... rest of logic
      }
    }
    </script>
    

    Required Changes:

    <script lang="ts">
    export default class DIDView extends Vue {
      activeDid = ""; // Component data still needed
    
      async mounted() {
        await this.initializeSettings();
        await this.determineDIDToDisplay();
        // ... rest of initialization
      }
    
      private async initializeSettings() {
        // This will automatically work if $accountSettings() is updated
        const settings = await this.$accountSettings();
        this.activeDid = settings.activeDid || "";
        this.apiServer = settings.apiServer || "";
      }
    
      // Add watcher for activeDid changes
      async onActiveDidChanged(newDid: string | null) {
        if (newDid && newDid !== this.activeDid) {
          this.activeDid = newDid;
          // Re-initialize if needed
          await this.determineDIDToDisplay();
        }
      }
    }
    </script>
    
  3. HomeView.vue - ActiveDid change detection

    • Current: Has onActiveDidChanged() watcher method
    • Impact: Watcher logic needs updates for new data source
    • Risk: MEDIUM - Core navigation component

    Current Implementation:

    <script lang="ts">
    export default class HomeView extends Vue {
      async onActiveDidChanged(newDid: string | null, oldDid: string | null) {
        if (newDid !== oldDid) {
          // Re-initialize identity with new settings (loads settings internally)
          await this.initializeIdentity();
        } else {
          logger.debug(
            "[HomeView Settings Trace] 📍 DID unchanged, skipping re-initialization",
          );
        }
      }
    
      private async initializeIdentity() {
        // ... identity initialization logic
        const settings = await this.$accountSettings();
        this.activeDid = settings.activeDid || "";
        // ... rest of initialization
      }
    }
    </script>
    

    Required Changes:

    <script lang="ts">
    export default class HomeView extends Vue {
      async onActiveDidChanged(newDid: string | null, oldDid: string | null) {
        if (newDid !== oldDid) {
          // This will automatically work if $accountSettings() is updated
          await this.initializeIdentity();
        } else {
          logger.debug(
            "[HomeView Settings Trace] 📍 DID unchanged, skipping re-initialization",
          );
        }
      }
    
      private async initializeIdentity() {
        // ... identity initialization logic
        // This will automatically work if $accountSettings() is updated
        const settings = await this.$accountSettings();
        this.activeDid = settings.activeDid || "";
        // ... rest of initialization
      }
    }
    </script>
    

    Key Insight: HomeView will require minimal changes since it already uses the $accountSettings() method, which will be updated to handle the new table structure transparently.

Medium Impact Components

  1. InviteOneAcceptView.vue - Identity fallback logic

    • Current: Creates identity if no activeDid exists
    • Impact: Fallback logic needs to check new table
    • Risk: MEDIUM - Invite processing component

    Current Implementation:

    <script lang="ts">
    export default class InviteOneAcceptView extends Vue {
      activeDid = ""; // Component data
    
      async mounted() {
        // Load or generate identity
        const settings = await this.$accountSettings();
        this.activeDid = settings.activeDid || "";
    
        // Identity creation should be handled by router guard, but keep as fallback for deep links
        if (!this.activeDid) {
          logger.info(
            "[InviteOneAcceptView] No active DID found, creating identity as fallback",
          );
          this.activeDid = await generateSaveAndActivateIdentity();
        }
        // ... rest of initialization
      }
    }
    </script>
    

    Required Changes:

    <script lang="ts">
    export default class InviteOneAcceptView extends Vue {
      activeDid = ""; // Component data still needed
    
      async mounted() {
        // This will automatically work if $accountSettings() is updated
        const settings = await this.$accountSettings();
        this.activeDid = settings.activeDid || "";
    
        // Fallback logic remains the same - the API layer handles the new table
        if (!this.activeDid) {
          logger.info(
            "[InviteOneAcceptView] No active DID found, creating identity as fallback",
          );
          this.activeDid = await generateSaveAndActivateIdentity();
        }
        // ... rest of initialization
      }
    }
    </script>
    

    Key Insight: This component will work automatically since it uses $accountSettings(). The fallback logic doesn't need changes.

  2. ClaimView.vue - Settings retrieval

    • Current: Gets activeDid from $accountSettings()
    • Impact: Will automatically work if API is updated
    • Risk: LOW - Depends on API layer updates

    Current Implementation:

    <script lang="ts">
    export default class ClaimView extends Vue {
      activeDid = ""; // Component data
    
      async created() {
        const settings = await this.$accountSettings();
        this.activeDid = settings.activeDid || "";
        this.apiServer = settings.apiServer || "";
        // ... rest of initialization
      }
    
      // Helper method that uses activeDid
      didInfo(did: string): string {
        return serverUtil.didInfo(
          did,
          this.activeDid, // Uses component data
          this.allMyDids,
          this.allContacts,
        );
      }
    }
    </script>
    

    Required Changes:

    <script lang="ts">
    export default class ClaimView extends Vue {
      activeDid = ""; // Component data still needed
    
      async created() {
        // This will automatically work if $accountSettings() is updated
        const settings = await this.$accountSettings();
        this.activeDid = settings.activeDid || "";
        this.apiServer = settings.apiServer || "";
        // ... rest of initialization
      }
    
      // No changes needed - method will work automatically
      didInfo(did: string): string {
        return serverUtil.didInfo(
          did,
          this.activeDid, // Uses component data
          this.allMyDids,
          this.allContacts,
        );
      }
    }
    </script>
    

    Key Insight: This component requires zero changes since it already uses the proper API method. It's the lowest risk component.

  3. ContactAmountsView.vue - Uses phased-out method

    • Current: Uses $getSettings(MASTER_SETTINGS_KEY) (being phased out)
    • Impact: NO CHANGES REQUIRED - Will be updated when migrating to getMasterSettings
    • Risk: LOW - Part of planned refactoring, not migration-specific

    Note: This component will be updated as part of the broader refactoring to replace $getSettings(MASTER_SETTINGS_KEY) with getMasterSettings(), which is separate from the activeDid migration. The migration plan focuses only on components that require immediate changes for the active_identity table.

Service Layer Impact

  1. WebPlatformService.ts

    • Current: Direct SQL queries to settings table
    • Impact: Must add active_identity table queries
    • Risk: HIGH - Core web platform service
  2. CapacitorPlatformService.ts

    • Current: Similar direct SQL access
    • Impact: Same updates as web service
    • Risk: HIGH - Mobile platform service
  3. PlatformServiceMixin.ts

    • Current: Core methods like $accountSettings(), $saveSettings()
    • Impact: Major refactoring required
    • Risk: CRITICAL - Used by 50+ components

API Contract Changes

  1. $saveSettings() method

    • Current: Updates settings.activeDid
    • New: Updates active_identity.activeDid
    • Impact: All components using this method
  2. $updateActiveDid() method

    • Current: Internal tracking only
    • New: Database persistence required
    • Impact: Identity switching logic

Master Settings Functions Impact

  1. retrieveSettingsForDefaultAccount() function

    • Current: Returns settings with activeDid field from master settings
    • New: Returns settings without activeDid field
    • Impact: HIGH - Used by migration scripts and core database utilities
    • Location: src/db/databaseUtil.ts:148
  2. $getMergedSettings() method

    • Current: Merges default and account settings, includes activeDid from defaults
    • New: Merges settings without activeDid, adds from active_identity table
    • Impact: HIGH - Core method used by $accountSettings()
    • Location: src/utils/PlatformServiceMixin.ts:485

Note: $getSettings(MASTER_SETTINGS_KEY) is being phased out in favor of getMasterSettings, so it doesn't require updates for this migration.

New getMasterSettings() Function

Since we're phasing out $getSettings(MASTER_SETTINGS_KEY), this migration provides an opportunity to implement the new getMasterSettings() function that will handle the active_identity table integration from the start:

// New getMasterSettings function to replace phased-out $getSettings
async getMasterSettings(): Promise<Settings> {
  try {
    // Get master settings without activeDid
    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;
    }

    const settings = this._mapColumnsToValues(result.columns, result.values)[0] as Settings;

    // Handle JSON field parsing
    if (settings.searchBoxes) {
      settings.searchBoxes = this._parseJsonField(settings.searchBoxes, []);
    }

    // Get activeDid from separate table
    const activeIdentityResult = await this.$dbQuery(
      "SELECT activeDid FROM active_identity WHERE id = 1"
    );

    if (activeIdentityResult?.values?.length) {
      settings.activeDid = activeIdentityResult.values[0][0] as string;
    }

    return settings;
  } catch (error) {
    logger.error(`[Settings Trace] ❌ Failed to get master settings:`, { error });
    return DEFAULT_SETTINGS;
  }
}

Testing Impact

  1. Unit Tests

    • All platform service methods
    • PlatformServiceMixin methods
    • Database migration scripts
  2. Integration Tests

    • Component behavior with new data source
    • Identity switching workflows
    • Settings persistence
  3. Platform Tests

    • Web, Electron, iOS, Android
    • Cross-platform data consistency
    • Migration success on all platforms

Performance Impact

  1. Additional Table Join

    • Settings queries now require active_identity table
    • Potential performance impact on frequent operations
    • Need for proper indexing
  2. Caching Considerations

    • ActiveDid changes trigger cache invalidation
    • Component re-rendering on identity switches
    • Memory usage for additional table data

Risk Assessment by Component Type

  • Critical Risk: PlatformServiceMixin, Platform Services
  • High Risk: Identity-related components, views using $accountSettings()
  • Medium Risk: Components with direct settings access, identity management
  • Low Risk: Components using only basic settings, utility components, prop-based components, components using phased-out methods

Migration Timeline Impact

  • Phase 1: Schema Creation - No component impact
  • Phase 2: Data Migration - No component impact
  • Phase 3: API Updates - All components affected
  • Phase 4: Cleanup - No component impact

Update Priority Order

  1. PlatformServiceMixin - Core dependency for most components
  2. Platform Services - Ensure data access layer works
  3. Identity Components - Verify core functionality
  4. Settings-Dependent Views - Update in dependency order
  5. Utility Components - Final cleanup and testing

Deferred for depth

  • Advanced identity management features enabled by this change
  • Performance optimization strategies for the new table structure
  • Future schema evolution planning
  • Advanced rollback and recovery procedures