Skip to main content
Vantage is offline-first. Every transaction is written to an encrypted local SQLite database before it ever touches the network. The app is fully usable without internet — data syncs automatically when connectivity returns.

Local database

Library: sqflite_sqlcipher — SQLite with AES-256 encryption via SQLCipher. Encryption key: Injected at build time via --dart-define:
The key never appears in source code or any tracked file.

Schema

Indexes:

Singleton and recovery

LocalDbService uses a Completer-based singleton — the database is opened once and reused:
Auto-recovery: If the database fails to open (wrong encryption key, legacy unencrypted file, corruption), the service deletes the file and recreates it. Data is re-synced from Supabase on the next pull.

Bidirectional sync

SyncService pushes local changes to Supabase and pulls remote changes to SQLite. Both directions are wrapped in database transactions for atomicity.

Push — local → Supabase

Pull — Supabase → local

Conflict resolution

Last-write-wins. Server data takes precedence on pull — ConflictAlgorithm.replace overwrites local rows with the same id. There is no merge or three-way diff. This is intentional: Supabase is the source of truth; local is a cache + write buffer.

Pending notifications queue

When a banking notification arrives but both regex parsing and Haiku AI parsing fail (e.g. no network for the AI call), the raw notification is stored in pending_notifications for retry:
On the next app launch, the notification service drains this queue and retries parsing against the now-available AI endpoint. Successfully parsed entries are marked processed=1.

App lifecycle sync triggers

VantageApp prefetches all major providers on init and on every app resume:

Credit card balance reconciliation

On every resume, Vantage recalculates all credit card balances from the transaction ledger:
The current billing cycle starts the day after the statement_day on the account. This corrects any drift between the stored balance field and what the transactions actually show — no manual reconciliation needed.