ArchitectureDatabase

Database

Comma stores all data in a local SQLite database, accessed through Drizzle ORM.

Database file: comma.db in the app sandbox.
Schema location: src/database/schema.ts
Queries: src/database/queries/
Client init: src/database/client.ts


Schema

vehicles

ColumnTypeNotes
idTEXT (PK)UUID
nameTEXTDisplay name
typeTEXTcar | bike | scooter | van | other
isActiveBOOLEANSoft-delete for UI
createdAtTIMESTAMP
makeTEXTOptional
modelTEXTOptional
yearINTEGEROptional
fuelTypeTEXTgas | electric | hybrid | other
licensePlateTEXTOptional
currentOdometerINTEGERRunning total, in miles/km
syncUpdatedAtINTEGERepoch ms — LWW sync clock
syncDeletedAtINTEGERepoch ms — soft-delete tombstone

shifts

ColumnTypeNotes
idTEXT (PK)UUID
vehicleIdTEXT (FK → vehicles)
platformTEXTPrimary platform ID
startTimeTIMESTAMP
endTimeTIMESTAMP
grossRevenueREALBase pay (not including tips)
tipsRevenueREALTips
activeMileageREALGPS-tracked delivery miles
deadMileageREALGPS-tracked commute/wait miles
trackedMileageREALDeprecated — kept for backward compat
durationSecondsINTEGERTotal elapsed seconds
pausedSecondsINTEGERPaused seconds (net = duration - paused)
notesTEXTOptional
routePathTEXTEncoded polyline (GPS route)
reconciliationStatusTEXTtracking | pending_reconciliation | reconciled
startOdometerINTEGEROptional manual odometer
endOdometerINTEGEROptional manual odometer
distanceSourceTEXTgps_only | odometer | manual
syncUpdatedAtINTEGER
syncDeletedAtINTEGER

shiftPlatforms

Per-platform sub-record for multi-platform shifts.

ColumnTypeNotes
idTEXT (PK)
shiftIdTEXT (FK → shifts, CASCADE)
platformTEXTPlatform ID
platformOnlineSecondsINTEGEROnline time for this platform
grossRevenueREALEarnings from this platform
tipsRevenueREALTips from this platform
tripsCountINTEGERNumber of deliveries
syncUpdatedAtINTEGER
syncDeletedAtINTEGER

expenses

ColumnTypeNotes
idTEXT (PK)
shiftIdTEXT (FK → shifts)Optional — link to a specific shift
vehicleIdTEXT (FK → vehicles)Optional
categoryTEXTExpense category key
amountREALTotal cost
dateTIMESTAMP
isDeductibleBOOLEANWhether any portion is deductible
deductiblePctREAL0–100; actual deduction = amount × pct/100
notesTEXT
receiptUriTEXTLocal file URI for receipt photo
isRecurringBOOLEAN
recurringIntervalTEXTweekly | monthly | yearly
merchantTEXTDisplay name
merchantNormalizedTEXTNormalized for grouping
syncUpdatedAtINTEGER
syncDeletedAtINTEGER

vehicles

(See above.)


maintenanceLogs

ColumnTypeNotes
idTEXT (PK)
vehicleIdTEXT (FK → vehicles)
typeTEXToil_change | tire | brake | fuel | wash | other
costREAL
odometerREALOptional reading at time of service
dateTIMESTAMP
notesTEXT
syncUpdatedAtINTEGER
syncDeletedAtINTEGER

goals

ColumnTypeNotes
idTEXT (PK)
labelTEXT
targetValueREAL
unitTEXTcurrency | hours | shifts | mileage
periodTEXTdaily | weekly | monthly | yearly
isActiveBOOLEAN
createdAtTIMESTAMP
syncUpdatedAtINTEGER
syncDeletedAtINTEGER

platforms

User’s platform configuration (active/inactive, rates, display order).

ColumnTypeNotes
idTEXT (PK)Platform key (e.g. doordash)
labelTEXTDisplay name
colorTEXTHex color
textColorTEXTText color on badge
countryTEXTCA | US | UK | NP
isActiveBOOLEANUser has activated this platform
hourlyRateTEXTTarget hourly rate
mileageRateTEXTPer-mile/km rate
sortPriorityINTEGEROrder in pickers
logoEmojiTEXTOptional emoji logo
syncUpdatedAtINTEGER
syncDeletedAtINTEGER

merchants

Normalized merchant names for expense grouping.

ColumnTypeNotes
idTEXT (PK)
nameTEXT (UNIQUE)Display name
normalizedNameTEXTLowercased, stripped for matching
syncUpdatedAtINTEGER
syncDeletedAtINTEGER

taxHistory

Append-only log of tax region/rate changes.

ColumnTypeNotes
idTEXT (PK)
oldRegionTEXTPrevious province/state
oldRateREALPrevious rate
newRegionTEXTNew province/state
newRateREALNew rate
changedAtTIMESTAMP
syncUpdatedAtINTEGER
syncDeletedAtINTEGER

vehicleTaxProfiles

ColumnTypeNotes
idTEXT (PK)
vehicleIdTEXT (FK → vehicles, CASCADE)
taxYearINTEGERe.g. 2025
countryTEXTUS | CA | UK | NP
deductionMethodTEXTstandard_mileage | actual_expenses
standardRatePrimaryREALIRS/CRA/HMRC rate (first tier)
standardRateSecondaryREALSecond-tier rate (UK: after 10k miles)
rateThresholdREALMiles at which rate steps down
beginningYearOdometerINTEGEROdometer Jan 1
endingYearOdometerINTEGEROdometer Dec 31
syncUpdatedAtINTEGER
syncDeletedAtINTEGER

settings

Key-value store for app configuration.

ColumnTypeNotes
keyTEXT (PK)Settings key
valueTEXTJSON-serialized value

Key examples: profile, active_shift_state, sync_device_id, sync_applied_logs, sync_last_pushed_at, sync_enabled


locationPoints

GPS points recorded during GPS-tracked shifts.

ColumnTypeNotes
idTEXT (PK)
sessionIdTEXTShift session identifier
shiftIdTEXT (FK → shifts)
latitudeREAL
longitudeREAL
altitudeREAL
accuracyREALGPS horizontal accuracy (meters)
speedREALm/s at time of point
timestampTIMESTAMP
sourceTEXTgps (default)
isFilteredBOOLEANtrue = discarded by jitter filter

Not synced to cloud. Device-local ephemeral data.


tempNativePoints

Staging table for the native GPS module. The native Kotlin/Swift code writes raw GPS points here; JS polls and processes them into locationPoints.

ColumnTypeNotes
idINTEGER (PK, autoincrement)
latREAL
lonREAL
timestampINTEGERepoch ms

Not synced.


syncOverwriteLog

Local audit trail for cloud sync merge conflicts on financial data. Device-local — not synced.

ColumnTypeNotes
idTEXT (PK)
tableNameTEXTWhich table had the conflict
rowIdTEXTThe row’s primary key
supersededRowTEXTJSON of the local row that lost
winnerRowTEXTJSON of the incoming row that won
mergedAtINTEGERepoch ms

Sync columns

Every synced table has two additional columns:

syncUpdatedAt: integer  // epoch ms of last local mutation — the LWW clock
syncDeletedAt: integer  // epoch ms of soft-delete, null if alive

Every read query filters WHERE syncDeletedAt IS NULL. Every write touches syncUpdatedAt.

See Cloud Sync for design details.


Migrations

Migrations are defined in src/database/client.ts using Drizzle’s migration system. When the app launches, it runs any pending migrations before the first query.

To add a migration: add a new entry to the migrations array in client.ts with the SQL to run and an incremented version number. Migrations are idempotent — they check whether the change already exists before applying it.


Query conventions

  • All queries live in src/database/queries/ — one file per domain (analytics, shifts, expenses, etc.).
  • No raw SQL strings in screen components. All SQL is in query files.
  • No JavaScript-level filtering of data that should be filtered in SQL (e.g. no .filter() on a result set that could use WHERE).
  • Mutations use syncedInsert / syncedUpdate / syncedDelete from src/database/syncedWrites.ts to automatically stamp sync columns.