Android Room Local-First

This is the minimum shape for an Android local-first store: Room tables, explicit migrations, export hooks, and no destructive fallback for user-owned data.

Database

@Database(
    entities = [TransactionEntity::class, PendingMutationEntity::class],
    version = 2,
    exportSchema = true
)
@TypeConverters(InstantConverter::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun transactions(): TransactionDao
    abstract fun pendingMutations(): PendingMutationDao
}

Builder

fun buildDatabase(context: Context): AppDatabase {
    return Room.databaseBuilder(
        context.applicationContext,
        AppDatabase::class.java,
        "app.db"
    )
        .addMigrations(MIGRATION_1_2)
        .build()
}

Do not add fallbackToDestructiveMigration() for user-owned data.

Export Hook

suspend fun exportTransactions(db: AppDatabase, sink: BufferedSink) {
    sink.writeUtf8("id,occurred_at,amount_cents,currency,merchant\n")
    db.transactions().getAllForExport().forEach { row ->
        sink.writeUtf8("${row.id},${row.occurredAt},${row.amountCents},${row.currency},${row.merchant.orEmpty()}\n")
    }
}

For privacy-first finance apps, CSV export is table stakes. Full backup can come later, but export should ship early.

Release Gate

  • migration test from every supported version to current
  • DAO tests on device/instrumented SQLite
  • CSV export test with thousands of rows
  • restore/import smoke test if backup exists
  • app lock and key management reviewed for private data
  • offline launch works with no network