Documentation

Architecture

How Alkemi works under the hood — for the technically curious.

Stack

Alkemi is an Android-only app built with React Native and Expo. There is no backend server, no cloud database, and no telemetry service.

LayerTechnology
FrameworkReact Native + Expo (managed workflow, custom dev client)
Databaseexpo-sqlite — on-device SQLite with WAL mode
StateZustand (client state) + TanStack Query (DB cache)
Navigationexpo-router (file-based)
Encryptionreact-native-libsodium (XChaCha20-Poly1305 + Argon2id)
Biometricsexpo-local-authentication
SMS captureAndroid BroadcastReceiver (native module)
Notification captureAndroid NotificationListenerService

Data flow

Every transaction follows the same path regardless of how it was captured:

1
Source

Bank SMS arrives via BroadcastReceiver, or a notification fires from a trusted app via NotificationListenerService.

2
Parse

ParserRulesEngine applies regex patterns to extract amount, merchant, account number, and direction (debit/credit). Learned corrections from parserLearning.ts are applied on top.

3
Orchestrate

ExpenseOrchestrator.addExpense validates the parsed result, runs deduplication (content hash + time window), resolves the account (falling back to the default Cash account), and delegates to the repository layer.

4
Persist

ExpenseRepository inserts the transaction into SQLite (amounts stored as integer paise — rupees × 100). AccountOrchestrator adjusts the account balance.

5
Surface

TanStack Query invalidates the relevant cache keys. The UI re-renders with the new transaction visible in the feed and reflected in budget/analytics.

SMS & notification parsing

The parser (src/services/parser.ts) uses a two-pass approach:

  1. Sender matching — the SMS sender ID or notification package name is checked against a list of ~30 trusted senders/apps. Unknown senders are ignored.
  2. Pattern matching — 50+ regex patterns cover Indian bank SMS formats. Patterns extract amount, merchant name, last-4 card digits, and transaction direction.

When the user corrects a parsed transaction (wrong merchant, wrong category), parserLearning.ts records the correction keyed to the sender. On the next matching SMS from that sender, the correction is applied before the generic patterns run.

Deduplication uses a content hash over (amount, direction, account, timestamp-window) to prevent the same transaction appearing twice when both SMS and a notification fire for it.

Storage & schema

All data lives in a single SQLite file (alkemi.db) in the app's private data directory. Key tables:

TablePurposeMoney column
transactionsMain ledgeramount INTEGER (paise)
accountsBank accounts, cards, wallets, cash
account_balance_eventsBalance anchors from SMSbalance INTEGER (paise)
goalsSavings goalstarget_amount, current_amount (paise)
debtsWho owes whomamount INTEGER (paise)
profilesMulti-profile support
kv_storeSettings, flags, onboarding state
schema_migrationsMigration version tracking

Amounts are always stored as integer paise (rupees × 100). Conversion happens exclusively at the repository boundary — mapRowToExpense divides by 100 on read, and INSERT/UPDATE multiply by 100 on write. This avoids floating-point rounding errors on aggregation.

The schema evolved through 6 forward-only migrations. PRAGMA foreign_keys = ON is set on every database open. A CHECK constraint on transactions.direction enforces that transfer rows always have both counter_account_id and entry_group_id.

Encryption

The on-device database is not encrypted at rest — it relies on the Android app sandbox for isolation. Field-level encryption was considered and removed; only backups are encrypted.

When you export a backup:

  1. A random 16-byte salt is generated.
  2. Your password is run through Argon2id (64 MiB memory, opslimit=2) to derive a 256-bit key.
  3. The database is encrypted with XChaCha20-Poly1305 (IETF variant) — authenticated encryption that detects tampering.
  4. The encrypted file (.alkemi extension) is passed to the native Android share sheet. Alkemi never sees where it goes.
cipher: XChaCha20-Poly1305 (IETF, 256-bit key)
kdf: Argon2id13 · m=65536 · t=2 · p=1
salt: 16 bytes (crypto_random)
library: react-native-libsodium

Restore decrypts the file in memory, verifies the authentication tag, and replaces the local database. A wrong password is detected immediately by the failed AEAD tag verification.

Privacy model

Privacy in Alkemi is architectural, not policy-based:

  • No INTERNET permission in the Android manifest for financial data access — the app cannot make outbound HTTP calls for your data even if compromised code were injected.
  • No analytics SDK — Crashlytics, Firebase, Mixpanel, and similar are not present in the dependency tree.
  • No telemetry — a remote logger stub exists in the codebase but is never wired to a transport.
  • Minimal permissions — only SMS, notification listener, and foreground service. No location, no contacts, no camera.

Invariants

These are correctness rules enforced in code. Breaking one causes a data bug, not just a style issue.

  • Amounts are integer paise in the DB. Rupees (float) only in TS/UI. Never aggregate rupee floats — SUM paise, divide at the end.
  • direction enum is debit | credit | transfer_out | transfer_in. Transfer rows require both counter_account_id and entry_group_id (enforced by DB CHECK).
  • transactions.account_id is NOT NULL. "Unassign" means reassign to the default Cash account — never SET NULL.
  • Every query is profile-scoped via ScopedQueryBuilder (profile_id = 'default').
  • Credit-card bill payments are stored as credit with is_excluded = 1 — never as a transfer.