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
andsrc/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
- Database Migration Guide
- Dexie to SQLite Mapping
- PlatformServiceMixin Documentation
- Migration Templates
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
-
IdentitySection.vue
- ReceivesactiveDid
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: </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. - Current: Uses
-
DIDView.vue
- Heavy activeDid usage- Current: Initializes
activeDid
inmounted()
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>
- Current: Initializes
-
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. - Current: Has
Medium Impact Components
-
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. - Current: Creates identity if no
-
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.
- Current: Gets
-
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)
withgetMasterSettings()
, which is separate from the activeDid migration. The migration plan focuses only on components that require immediate changes for the active_identity table. - Current: Uses
Service Layer Impact
-
WebPlatformService.ts
- Current: Direct SQL queries to settings table
- Impact: Must add
active_identity
table queries - Risk: HIGH - Core web platform service
-
CapacitorPlatformService.ts
- Current: Similar direct SQL access
- Impact: Same updates as web service
- Risk: HIGH - Mobile platform service
-
PlatformServiceMixin.ts
- Current: Core methods like
$accountSettings()
,$saveSettings()
- Impact: Major refactoring required
- Risk: CRITICAL - Used by 50+ components
- Current: Core methods like
API Contract Changes
-
$saveSettings()
method- Current: Updates
settings.activeDid
- New: Updates
active_identity.activeDid
- Impact: All components using this method
- Current: Updates
-
$updateActiveDid()
method- Current: Internal tracking only
- New: Database persistence required
- Impact: Identity switching logic
Master Settings Functions Impact
-
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
- Current: Returns settings with
-
$getMergedSettings()
method- Current: Merges default and account settings, includes
activeDid
from defaults - New: Merges settings without
activeDid
, adds fromactive_identity
table - Impact: HIGH - Core method used by
$accountSettings()
- Location:
src/utils/PlatformServiceMixin.ts:485
- Current: Merges default and account settings, includes
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
-
Unit Tests
- All platform service methods
- PlatformServiceMixin methods
- Database migration scripts
-
Integration Tests
- Component behavior with new data source
- Identity switching workflows
- Settings persistence
-
Platform Tests
- Web, Electron, iOS, Android
- Cross-platform data consistency
- Migration success on all platforms
Performance Impact
-
Additional Table Join
- Settings queries now require active_identity table
- Potential performance impact on frequent operations
- Need for proper indexing
-
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
- PlatformServiceMixin - Core dependency for most components
- Platform Services - Ensure data access layer works
- Identity Components - Verify core functionality
- Settings-Dependent Views - Update in dependency order
- 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