9.9 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	Secure Storage Implementation Guide for TimeSafari App
Overview
This document outlines the implementation of secure storage for the TimeSafari app. The implementation focuses on:
- 
Platform-Specific Storage Solutions:
- Web: SQLite with IndexedDB backend (absurd-sql)
 - Electron: SQLite with Node.js backend
 - Native: (Planned) SQLCipher with platform-specific secure storage
 
 - 
Key Features:
- SQLite-based storage using absurd-sql for web
 - Platform-specific service factory pattern
 - Consistent API across platforms
 - Migration support from Dexie.js
 
 
Quick Start
1. Installation
# Core dependencies
npm install @jlongster/sql.js
npm install absurd-sql
# Platform-specific dependencies (for future native support)
npm install @capacitor/preferences
npm install @capacitor-community/biometric-auth
2. Basic Usage
// Using the platform service
import { PlatformServiceFactory } from '../services/PlatformServiceFactory';
// Get platform-specific service instance
const platformService = PlatformServiceFactory.getInstance();
// Example database operations
async function example() {
  try {
    // Query example
    const result = await platformService.dbQuery(
      "SELECT * FROM accounts WHERE did = ?",
      [did]
    );
    // Execute example
    await platformService.dbExec(
      "INSERT INTO accounts (did, public_key_hex) VALUES (?, ?)",
      [did, publicKeyHex]
    );
    } catch (error) {
    console.error('Database operation failed:', error);
  }
}
3. Platform Detection
// src/services/PlatformServiceFactory.ts
export class PlatformServiceFactory {
  static getInstance(): PlatformService {
    if (process.env.ELECTRON) {
      // Electron platform
      return new ElectronPlatformService();
    } else {
      // Web platform (default)
      return new AbsurdSqlDatabaseService();
    }
  }
}
4. Current Implementation Details
Web Platform (AbsurdSqlDatabaseService)
The web platform uses absurd-sql with IndexedDB backend:
// src/services/AbsurdSqlDatabaseService.ts
export class AbsurdSqlDatabaseService implements PlatformService {
  private static instance: AbsurdSqlDatabaseService | null = null;
  private db: AbsurdSqlDatabase | null = null;
  private initialized: boolean = false;
      
  // Singleton pattern
  static getInstance(): AbsurdSqlDatabaseService {
    if (!AbsurdSqlDatabaseService.instance) {
      AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
    }
    return AbsurdSqlDatabaseService.instance;
  }
  // Database operations
  async dbQuery(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
    await this.waitForInitialization();
    return this.queueOperation<QueryExecResult[]>("query", sql, params);
    }
  async dbExec(sql: string, params: unknown[] = []): Promise<void> {
    await this.waitForInitialization();
    await this.queueOperation<void>("run", sql, params);
  }
}
Key features:
- Uses absurd-sql for SQLite in the browser
 - Implements operation queuing for thread safety
 - Handles initialization and connection management
 - Provides consistent API across platforms
 
5. Migration from Dexie.js
The current implementation supports gradual migration from Dexie.js:
// Example of dual-storage pattern
async function getAccount(did: string): Promise<Account | undefined> {
  // Try SQLite first
  const platform = PlatformServiceFactory.getInstance();
  let account = await platform.dbQuery(
    "SELECT * FROM accounts WHERE did = ?",
    [did]
  );
  // Fallback to Dexie if needed (migration period only)
  // Note: This fallback is only used during the migration period
  // and will be removed once migration is complete
  return account;
   }
A. Modifying Code
When converting from Dexie.js to SQL-based implementation, follow these patterns:
- 
Database Access Pattern
// Before (Dexie) const result = await db.table.where("field").equals(value).first(); // After (SQL) const platform = PlatformServiceFactory.getInstance(); let result = await platform.dbQuery( "SELECT * FROM table WHERE field = ?", [value] ); result = databaseUtil.mapQueryResultToValues(result); // Fallback to Dexie if needed (migration period only) // Note: This fallback is only used during the migration period // and will be removed once migration is complete - 
Update Operations
// Before (Dexie) await db.table.where("id").equals(id).modify(changes); // After (SQL) // For settings updates, use the utility methods: await databaseUtil.updateDefaultSettings(changes); // OR await databaseUtil.updateAccountSettings(did, changes); // For other tables, use direct SQL: const platform = PlatformServiceFactory.getInstance(); await platform.dbExec( "UPDATE table SET field1 = ?, field2 = ? WHERE id = ?", [changes.field1, changes.field2, id] ); // Fallback to Dexie if needed (migration period only) // Note: This fallback is only used during the migration period // and will be removed once migration is complete - 
Insert Operations
// Before (Dexie) await db.table.add(item); // After (SQL) const platform = PlatformServiceFactory.getInstance(); const columns = Object.keys(item); const values = Object.values(item); const placeholders = values.map(() => '?').join(', '); const sql = `INSERT INTO table (${columns.join(', ')}) VALUES (${placeholders})`; await platform.dbExec(sql, values); // Fallback to Dexie if needed (migration period only) // Note: This fallback is only used during the migration period // and will be removed once migration is complete - 
Delete Operations
// Before (Dexie) await db.table.where("id").equals(id).delete(); // After (SQL) const platform = PlatformServiceFactory.getInstance(); await platform.dbExec("DELETE FROM table WHERE id = ?", [id]); // Fallback to Dexie if needed (migration period only) // Note: This fallback is only used during the migration period // and will be removed once migration is complete - 
Result Processing
// Before (Dexie) const items = await db.table.toArray(); // After (SQL) const platform = PlatformServiceFactory.getInstance(); let items = await platform.dbQuery("SELECT * FROM table"); items = databaseUtil.mapQueryResultToValues(items); // Fallback to Dexie if needed (migration period only) // Note: This fallback is only used during the migration period // and will be removed once migration is complete - 
Using Utility Methods
 
When working with settings or other common operations, use the utility methods in db/index.ts:
// Settings operations
await databaseUtil.updateDefaultSettings(settings);
await databaseUtil.updateAccountSettings(did, settings);
const settings = await databaseUtil.retrieveSettingsForDefaultAccount();
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
// Logging operations
await databaseUtil.logToDb(message);
await databaseUtil.logConsoleAndDb(message, showInConsole);
Key Considerations:
- Always use 
databaseUtil.mapQueryResultToValues()to process SQL query results - Use utility methods from 
db/index.tswhen available instead of direct SQL - Keep Dexie fallbacks wrapped in migration period checks
 - For queries that return results, use 
letvariables to allow Dexie fallback to override - For updates/inserts/deletes, execute both SQL and Dexie operations during migration period
 
Example Migration:
// Before (Dexie)
export async function updateSettings(settings: Settings): Promise<void> {
  await db.settings.put(settings);
}
// After (SQL)
export async function updateSettings(settings: Settings): Promise<void> {
  const platform = PlatformServiceFactory.getInstance();
  const { sql, params } = generateUpdateStatement(
    settings,
    "settings",
    "id = ?",
    [settings.id]
  );
  await platform.dbExec(sql, params);
}
Remember to:
- 
Create database access code to use the platform service, putting it in front of the Dexie version
 - 
Instead of removing Dexie-specific code, keep it.
- 
For creates & updates & deletes, the duplicate code is fine.
 - 
For queries where we use the results, make the setting from SQL into a 'let' variable, then wrap the Dexie code in a migration period check and if it's during migration then use that result instead of the SQL code's result.
 
 - 
 - 
Consider data migration needs, and warn if there are any potential migration problems
 
Success Criteria
- 
Functionality
- Basic CRUD operations work correctly
 - Platform service factory pattern implemented
 - Error handling in place
 - Native platform support (planned)
 
 - 
Performance
- Database operations complete within acceptable time
 - Operation queuing for thread safety
 - Proper initialization handling
 - Performance monitoring (planned)
 
 - 
Security
- Basic data integrity
 - Encryption (planned for native platforms)
 - Secure key storage (planned)
 - Platform-specific security features (planned)
 
 - 
Testing
- Basic unit tests
 - Comprehensive integration tests (planned)
 - Platform-specific tests (planned)
 - Migration tests (planned)
 
 
Next Steps
- 
Native Platform Support
- Implement SQLCipher for iOS/Android
 - Add platform-specific secure storage
 - Implement biometric authentication
 
 - 
Enhanced Security
- Add encryption for sensitive data
 - Implement secure key storage
 - Add platform-specific security features
 
 - 
Testing and Monitoring
- Add comprehensive test coverage
 - Implement performance monitoring
 - Add error tracking and analytics
 
 - 
Documentation
- Add API documentation
 - Create migration guides
 - Document security measures