ArchitectureArchitecture Overview

Architecture Overview

Design principles

  1. Local-first. No network required for any core feature. The SQLite database on the device is the single source of truth.
  2. No backend. Comma has no servers, no APIs, no user accounts. Google Drive is the only external service — and only if the user opts in.
  3. TypeScript strict. No any. The compiler catches data-shape bugs before users do.
  4. Queries via ORM. All database access goes through Drizzle. No raw SQL string-building; no JS-level filtering of data that belongs in a query.
  5. Native where it matters. GPS tracking and wake locks run in a native module (Kotlin/Swift) to guarantee reliability that pure JS cannot provide on mobile.

Tech stack

LayerTechnologyVersion
FrameworkReact Native0.85.3
UI layerReact19.2.3
Build toolExpo SDK56
RoutingExpo Router56.2.11
DatabaseSQLite via expo-sqlite56.0.5
ORMDrizzle ORM0.45.2
Global stateZustand5.0.9
Server state / cacheTanStack React Query5.90.11
StylingNativeWind v4 (Tailwind CSS)v4
AnimationsReact Native Reanimated4.3.1
GesturesReact Native Gesture Handler2.31.1
IconsLucide React Native1.21.0
Chartsreact-native-svg (custom)
Bottom sheets@gorhom/bottom-sheet5.2.8
Google auth@react-native-google-signin/google-signin16.1.2
HTTPaxios1.13.2
Cryptoreact-native-quick-crypto1.1.5
CSVpapaparse5.5.4

High-level architecture

┌───────────────────────────────────────────────────────┐
│                   Expo Router (file-based)             │
│   app/(tabs)/   app/shift/   app/settings/   etc.     │
└──────────────────────────┬────────────────────────────┘

          ┌────────────────┼────────────────┐
          ▼                ▼                ▼
   Zustand stores    React Query       Custom Hooks
   (global state)  (DB cache layer)   (GPS, sync, etc.)
          │                │
          └────────────────┘

          ┌────────▼────────┐
          │  Drizzle ORM    │
          │  (queries/)     │
          └────────┬────────┘

          ┌────────▼────────┐
          │  SQLite DB      │
          │  (local file)   │
          └─────────────────┘

   ┌──────────────────┐        ┌──────────────────┐
   │  Native Module   │        │  Google Drive    │
   │  (GPS service)   │        │  (backup/sync)   │
   │  Kotlin / Swift  │        │  appDataFolder   │
   └──────────────────┘        └──────────────────┘

Data flow

Reading data

Screen component
  → calls React Query hook (e.g. useQuery(['shifts', 'recent']))
  → React Query checks cache (stale-while-revalidate)
  → if stale: calls queryFn (a Drizzle query in src/database/queries/)
  → Drizzle generates SQL → expo-sqlite executes → rows returned
  → React Query updates cache → component re-renders

Writing data

User action (e.g. "End Shift")
  → calls mutation (useMutation)
  → calls syncedInsert/syncedUpdate/syncedDelete (src/database/syncedWrites.ts)
     → stamps syncUpdatedAt = Date.now()
     → executes Drizzle insert/update/soft-delete
  → on success: invalidates relevant React Query keys
  → React Query re-fetches → UI updates

GPS tracking

Shift starts
  → useGPSTracking hook starts native module (comma-tracker)
  → Native module runs foreground service (Android) / background task (iOS)
  → GPS points written to tempNativePoints table by native code
  → JS polls tempNativePoints → applies jitter filter → appends to locationPoints
  → On shift end: calculates route distance, assigns to shift record

Folder map

commaApp/
├── app/                    # Expo Router routes (screens)
├── src/
│   ├── components/         # Reusable React components
│   ├── database/           # Drizzle schema, migrations, queries
│   ├── services/           # Business logic (GPS, backup, sync, gamification)
│   ├── registry/           # Static data (platforms, countries, badges)
│   ├── hooks/              # App-specific hooks
│   └── lib/                # Pure utilities
├── store/                  # Zustand stores
├── hooks/                  # Cross-cutting hooks (GPS, sync, backup)
├── providers/              # React context providers
├── modules/
│   └── comma-tracker/      # Native GPS module (Kotlin + Swift)
├── utils/                  # General utilities
└── docs/                   # This documentation

See Project Structure for the complete file tree.


Key architectural decisions

Why SQLite (not Realm, WatermelonDB, AsyncStorage)?

SQLite is the most battle-tested embedded database available. Drizzle ORM gives us type-safe queries and schema migrations. AsyncStorage is a key-value store — it doesn’t scale to relational financial data. WatermelonDB and Realm are both viable but add more complexity than the project needs.

Why Zustand (not Redux)?

Redux adds significant boilerplate. Zustand is ~1kb and provides the same capabilities for Comma’s use case (two small global stores). React Query handles the more complex server-state problem (cache invalidation, background refresh).

Why Expo Router (not React Navigation)?

File-based routing keeps the navigation structure readable at a glance. The folder structure matches the URL structure. Deep links map directly to files. Expo Router is built on React Navigation under the hood, so all the same primitives are available.

Why a native GPS module (not expo-location alone)?

expo-location works well for foreground location. For background tracking that survives screen-off and OS-level memory pressure, a dedicated foreground service (Android) or background task (iOS) is required. Comma’s native comma-tracker module handles this reliably.

Why no backend?

Privacy, simplicity, and cost. A backend means user accounts, authentication, servers to maintain, data to protect, and a subscription to justify the running cost. Comma’s target user is a gig worker who doesn’t want another app subscription. Local-first + optional Drive backup solves the real problem (data loss on phone change) without these tradeoffs.