Skip to main content

Drizzle ORM & Offline-First

The mobile app uses Drizzle ORM with expo-sqlite for a fully offline-first local database.

Architecture Layers

UI Components (React)
|
React Query Hooks (src/db/hooks/)
|
Repository Functions (src/db/repositories/)
|
Drizzle ORM (src/db/client.ts)
|
expo-sqlite (resibibo.db)

Schema

Table definitions live in src/db/schema/ using Drizzle's table builders:

TablePKNotes
categoriesINTEGER autoserverId for server mapping, isDefault flag
transactionsTEXT UUIDSoft delete via deletedAt, syncStatus column
transaction_line_itemsINTEGER autoFK to transactions (cascade)
budgetsTEXT UUIDperiod enum, isActive flag
recurring_rulesTEXT UUIDfrequency enum, nextDate
receiptsTEXT UUIDprocessingStatus, parsedData as JSON text

All tables with sync support have a syncStatus column with values: pending, syncing, synced, conflict.

Key Differences from Server

  • Categories use integer PK + serverId for server mapping
  • Other entities use text PK with client-generated UUIDs (via expo-crypto)
  • Monetary amounts stored as real (not decimal)
  • Dates stored as ISO 8601 text strings
  • No encrypted_data column (encryption happens at sync time)

Repositories

Repository functions in src/db/repositories/ are plain async functions that perform CRUD operations:

// Example: insert a transaction
export async function insertTransaction(db: DrizzleDB, data: NewTransaction) {
return db.insert(transactions).values({
...data,
syncStatus: "pending",
});
}

All write operations set syncStatus: 'pending' to flag records for sync.

React Query Hooks

Hooks in src/db/hooks/ wrap repositories with react-query-kit:

  • createQuery for reads (auto-caching, refetching)
  • createMutation for writes (invalidation on success)

Mutation hooks invalidate related queries. For example, inserting a transaction invalidates transactions, summaries, and budgets.

Migrations

Migrations are auto-generated by drizzle-kit generate and applied on app boot:

  1. Run pnpm db:generate after schema changes to generate SQL migration files
  2. Migration files are committed to src/db/migrations/
  3. On app start, DatabaseProvider runs useMigrations() from drizzle-orm/expo-sqlite/migrator
  4. After migrations, default categories are seeded if the table is empty

Metro config includes .sql in sourceExts so migration files are bundled correctly.

Database Provider

The DatabaseProvider component gates the entire app until:

  1. Migrations have been applied
  2. Default categories have been seeded (18 categories matching the server)

This ensures the app never renders with an uninitialized database.