Skip to main content

On This Page

Building Local-First Financial Apps with IndexedDB and Web Crypto

3 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Your Financial Data Should Live on Your Device. Here Is the Architecture That Makes That Possible.

Talliofi implements a local-first architecture where IndexedDB serves as the primary source of truth rather than a remote server. The system achieves a functional load time of under 200ms by eliminating the need for initial API calls and authentication handshakes.

Why This Matters

Standard web architectures treat the server as the canonical state, forcing users to accept privacy risks and latency as the inevitable cost of admission. By inverting this model, local-first systems ensure that applications remain fully functional during network outages and maintain data longevity even if the service provider ceases to exist, addressing the fundamental privacy paradox of modern financial software.

Key Insights

  • Local-first software treats data on the device as the source of truth and sync as a convenience, a principle defined by Ink & Switch in 2019.
  • Talliofi utilizes Dexie as a TypeScript wrapper for IndexedDB, managing an 11-table schema that supports sequential migrations and compound indexing.
  • The system implements 600,000 PBKDF2 iterations for key derivation, adhering to OWASP recommendations to mitigate brute-force attacks.
  • TanStack Query is utilized as an in-memory cache for local database queries rather than as a network fetching tool, optimizing rendering performance.
  • Conflict resolution follows a last-writer-wins strategy using a local changelog table to manage create, update, and delete operations asynchronously.

Working Examples

The IndexedDB schema implementation using Dexie for local-first data persistence.

export class TalliofiDatabase extends Dexie {
plans!: Table<Plan, string>;
expenses!: Table<ExpenseItem, string>;
goals!: Table<Goal, string>;
assets!: Table<Asset, string>;
liabilities!: Table<Liability, string>;
snapshots!: Table<MonthlySnapshot, string>;
netWorthSnapshots!: Table<NetWorthSnapshot, string>;
changelog!: Table<ChangeLogEntry, string>;
recurringTemplates!: Table<RecurringTemplate, string>;
attachments!: Table<ExpenseAttachment, string>;
vault!: Table<EncryptedVaultRecord, string>;
}

Client-side key derivation using the native Web Crypto API and OWASP-recommended PBKDF2 iterations.

export async function deriveKey(password: string, salt: Uint8Array): Promise<CryptoKey> {
  const encoder = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, ['deriveKey']);
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}

Practical Applications

  • Use case: Achieving sub-200ms application startup times by reading from local IndexedDB storage instead of remote APIs. Pitfall: Neglecting compound indexes in IndexedDB can cause performance degradation during large date-range queries.
  • Use case: Implementing zero-knowledge sync using a ‘Bring Your Own Supabase’ model to ensure the service provider never sees raw user data. Pitfall: Relying on external JavaScript crypto libraries instead of the native Web Crypto API increases the application’s attack surface and supply-chain risk.
  • Use case: Utilizing a repo router to switch between local, cloud, and encrypted storage modes without changing UI components. Pitfall: Storing large binary files like receipts as base64 strings in IndexedDB can lead to storage limit issues compared to dedicated blob storage.

References:

Continue reading

Next article

Bridging the Gap Between Side Projects and Startups in the AI Era

Related Content