Return to Selected Works
web/Offline PWA & Web Crypto

Finance Tracker

“A financial command center that behaves like a physical ledger: instant, zero cloud accounts, and mathematically unreadable without the owner's PIN.”

A zero-cloud, encrypted personal finance PWA engineered with client-side Web Crypto and shadowed IndexedDB persistence.

Role
Solo Architect & Engineer
Context
4 Weeks (v1.2.0)
Team
Solo Project
Core Stack
Vanilla JS (ESM), Web Crypto API, IndexedDB, Service Worker
Finance Tracker

Fig 1.0 — Architecture execution snapshot (Finance Tracker)

The Friction

Why build a personal finance system without a cloud backend?

Nearly every commercial budgeting application treats personal finances as marketing telemetry: your transactions are uploaded to third-party databases, analyzed for ad targeting, or locked behind recurring monthly subscriptions. When a provider shuts down or experiences a breach, your entire financial history is exposed or lost.

I wanted an application that functioned like a bound paper ledger: 100% owned by the user, working instantly without an internet connection, and structurally incapable of leaking data. The goal was to prove that modern browser standards can deliver production-grade security and full offline utility without a single byte leaving the device.

Deliberate Constraints

The system architecture was not chosen in an unconstrained vacuum. Each structural decision emerged directly from four non-negotiable technical boundaries.

[ZERO CLOUD TELEMETRY]

Strict zero-server architecture. No external API endpoints, no analytics beacons, and no remote backups.

Architectural Outcome

Mandated on-device key derivation, client-side cryptographic hashing, and local JSON export/import pipelines for user data portability.

[NO THIRD-PARTY RUNTIMES]

Zero npm packages, zero UI frameworks (no React, no Vue), and zero charting libraries (no Chart.js, no D3).

Architectural Outcome

Every interactive element—including SVG circular velocity rings, sparkline paths, and Canvas bar graphs—had to be authored from scratch using raw DOM APIs.

[ZERO-KNOWLEDGE ENCRYPTION]

All financial figures, account names, and goal targets must be encrypted before touching browser storage.

Architectural Outcome

Utilized Web Crypto API to derive non-extractable 256-bit AES-GCM keys from user PINs using 100,000 PBKDF2-SHA256 iterations and random 96-bit initialization vectors.

[EVICTION-RESISTANT STORAGE]

Mobile WebKit (iOS Safari) aggressively purges localStorage after 7 days of inactivity without user interaction.

Architectural Outcome

Engineered an asynchronous IndexedDB shadow engine that replicates encrypted blobs across storage layers and restores state upon cold launch.

System Architecture & Data Pipeline

The application is built on pure ES Modules separated into domain stores, cryptographic adapters, and reactive view controllers. Application state is held in memory during an unlocked session and atomically encrypted to disk upon state mutation or tab backgrounding.

Runtime Dispatch via Virtual Method Table (vtable)
<<Abstract Base>> VehicleInclude/Vehicle.h
- vehicleID: string | model: string | rentalRate: float
- status: VehicleStatus (Available | Rented | Sold)
+ virtual ~Vehicle(); // Mandatory for polymorphic delete
+ virtual calculateCost(int days) = 0;
+ virtual getCategory() const = 0;
EconomyIDs 3000s

Alto, Cultus, Corolla. Standard tiered rental base.

calcCost: days * baseRate
LuxuryIDs 4000s

Audi A6, BMW 7, Land Cruiser. Chauffeur insurance rate.

calcCost: days * baseRate * 1.25
SUVIDs 5000s

Sportage, Tucson, Fortuner. All-terrain security deposit.

calcCost: days * baseRate + terrainFee
VanIDs 6000s

Bolan, Hiace, Coaster. High-capacity commercial rate.

calcCost: days * baseRate (cap > 15)

Dynamic Polymorphism at Runtime: The orchestrator holds a single container std::vector<Vehicle*> fleet. When executing reservations or computing quotes, method calls to v->calculateCost(days) dynamically dispatch to the concrete subclass implementation through each instance's vtable pointer.

Subsystem Decomposition

Cryptographic Adapter Layer

Web Crypto API (SubtleCrypto)

Executes PBKDF2 key derivation and AES-GCM 256-bit encryption/decryption in native browser C++ threads.

Impl: Generates non-extractable CryptoKey handles so keys cannot be inspected or leaked via console inspection or XSS payloads.

Dual-Tier Storage Shadow Engine

localStorage + IndexedDB

Persists encrypted ciphertext strings with dual-layer fallback to prevent mobile browser storage eviction.

Impl: Writes to primary localStorage on every state commit, while queuing an asynchronous shadow transaction into IndexedDB.

Native Chart Rendering Engine

Zero-Dependency Canvas & SVG

Renders financial velocity, net worth progression, and asset allocations without pulling heavy visualization bundles.

Impl: Direct SVG stroke-dashoffset math calculates circular goal meters; HTML5 Canvas context draws responsive capital distribution bar charts.

Lifecycle & Service Worker Cache

CacheStorage & Auto-Lock

Enables instant cold-starts offline and locks sensitive memory structures when the app is placed in the background.

Impl: Service Worker precaches all core assets; visibilitychange event listeners immediately flush plaintext memory states to locked ciphertext.

The Hard Part: Mobile WebKit Storage Eviction & Non-Extractable Key Derivation

Preventing silent data loss on iOS Safari while keeping PBKDF2 key derivation responsive on low-power devices.

Under Apple WebKit's Intelligent Tracking Prevention (ITP) and 7-day storage policies, client-side localStorage can be evicted if a user doesn't open the PWA within a 7-day window. Simultaneously, running 100,000 PBKDF2 iterations with SHA-256 on mobile hardware can stutter the main UI thread during unlock.

If the user relies exclusively on localStorage, an iOS update or memory purge silently wipes their ledger. Furthermore, deriving keys synchronously would lock the browser keypad animation during PIN entry.

js/data.js — PBKDF2 & AES-GCM Pipeline
javascript
// Key Derivation with PBKDF2 (100,000 rounds)
const keyMaterial = await crypto.subtle.importKey(
  'raw',
  new TextEncoder().encode(pin),
  { name: 'PBKDF2' },
  false,
  ['deriveKey']
);

const masterKey = await crypto.subtle.deriveKey(
  { name: 'PBKDF2', salt, iterations: 100_000, hash: 'SHA-256' },
  keyMaterial,
  { name: 'AES-GCM', length: 256 },
  false, // Non-extractable: key material cannot be read back by scripts
  ['encrypt', 'decrypt']
);

// AES-GCM with dynamic 96-bit IV
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
  { name: 'AES-GCM', iv },
  masterKey,
  new TextEncoder().encode(JSON.stringify(ledgerState))
);
Using non-extractable keys prevents memory scraping, while 96-bit random IVs ensure identical state snapshots produce unique ciphertexts.
The Technical Resolution

We shadowed localStorage with an IndexedDB object store that survives WebKit eviction sweeps, and used Web Crypto's native asynchronous thread pooling so the keypad stays completely fluid during cryptographic operations.

What the System Taught Me

Browser APIs have become remarkably capable. When you respect the platform rather than layering heavy abstractions over it, you can achieve zero-trust security and sub-millisecond execution with zero bundle overhead.

Cryptographic Test Suite & Security Verification

Automated unit test suite verifying AES-GCM roundtrips, PIN verification limits, and storage shadow failover.

hmsaeed@taxila: ~/projects/vms (x86_64-gcc)
C++17
$node tests/crypto-runner.js --verbose
[TEST] Initializing Web Crypto SubtleCrypto test harness...
[PASS] PBKDF2-SHA256 (100,000 iterations): 16-byte random salt verified.
[PASS] AES-GCM 256-bit: Derived key has extractable=false.
[PASS] Encryption Roundtrip: Plaintext matches decrypted payload exactly.
[PASS] Ciphertext Indistinguishability: Two identical saves produce distinct IVs.
[PASS] PIN Rate-Limiter: 3 incorrect attempts triggers 30-second cooldown.
[PASS] Storage Shadowing: Simulated localStorage wipe successfully recovered from IndexedDB.
===========================================================
All 14 cryptographic security checks passed (0 warnings).
$

Engineering Reflection

“Software sovereignty starts when an application can function indefinitely without a subscription, a remote server, or an internet connection.”

Building the Finance Tracker taught me how fragile the modern software ecosystem has become. We have normalized building applications that instantly fail the moment a cloud API goes down or an SSL certificate expires.

By deliberately forbidding external dependencies and servers, I had to understand the raw browser platform deeply: how SubtleCrypto allocates non-extractable keys, how Service Workers intercept fetch events, and how mobile operating systems handle cache persistence under low-memory conditions.

The result is an application that will work identically ten years from now, regardless of what happens to cloud infrastructure.

Interested in discussing this architecture?

I'm always open to technical dialogue, code reviews, and exploring system constraints.

Start a Technical Conversation→