Local state lives in a Room database, with DataStore for lightweight
key/value preferences and a Paging 3 RemoteMediator layer to back paged lists
(activity feed, chat) from the network. The persistence layer is split into three
shared sub-modules so storage details don't leak into features.
Note on encryption: the Flipcash Room database is not wrapped with SQLCipher (there is no
SupportFactory/net.zeteticinapps/flipcash/shared/persistence). Instead, each account gets its own database file, named from the account entropy.
graph TD
Feature["Feature / shared coordinator"]
Provider[":shared:persistence:provider — access facade"]
Sources[":shared:persistence:sources — RemoteMediators, paged sources"]
Db[":shared:persistence:db — FlipcashDatabase, DAOs, entities"]
Room["Room (SQLite)"]
DataStore["DataStore (preferences)"]
Backend["gRPC backend"]
Feature --> Provider --> Sources --> Db --> Room
Provider --> DataStore
Sources --> Backend
| Module | Role |
|---|---|
:apps:flipcash:shared:persistence:db |
The Room FlipcashDatabase, entities, DAOs, type converters, migrations. |
:apps:flipcash:shared:persistence:sources |
Paging RemoteMediators and data sources that bridge network ↔ database. |
:apps:flipcash:shared:persistence:provider |
The injected facade features use to reach DAOs/DataStore without depending on Room directly. |
FlipcashDatabase
is a standard Room @Database (currently version 20) with a long chain of
@AutoMigrations and @TypeConverters for token and chat payloads:
@Database(
entities = [
MessageEntity::class, TokenEntity::class, SocialLinkEntity::class,
TokenValuationEntity::class, CurrencyCreatorDraftEntity::class,
ContactSyncStateEntity::class, ContactMappingEntity::class,
ChatMetadataEntity::class, ChatMessageEntity::class, ChatMemberEntity::class,
],
autoMigrations = [ /* 1->2 ... 19->20 */ ],
version = 20,
)
@TypeConverters(TokenTypeConverters::class, ChatTypeConverters::class)
abstract class FlipcashDatabase : RoomDatabase() {
abstract fun messageDao(): MessageDao
abstract fun tokenDao(): TokenDao
abstract fun contactDao(): ContactDao
abstract fun chatMetadataDao(): ChatMetadataDao
abstract fun chatMessageDao(): ChatMessageDao
abstract fun chatMemberDao(): ChatMemberDao
abstract fun currencyCreatorDraftDao(): CurrencyCreatorDraftDao
// ...
}The database is opened with a name derived from the signed-in account's entropy, so each user gets an isolated file and switching accounts can't cross-contaminate cached state:
fun init(context: Context, entropyB64: String) {
val dbUniqueName = Base58.encode(entropyB64.toByteArray().subByteArray(0, 6))
dbName = "$dbNamePrefix-$dbUniqueName.db" // e.g. fcash_database-<base58>.db
instance = Room.databaseBuilder(context, FlipcashDatabase::class.java, dbName)
.fallbackToDestructiveMigration()
.build()
}Room schema JSON is checked in under
apps/flipcash/shared/persistence/db/schemas/ to keep migrations honest.
DAOs (…/persistence/dao/) expose suspend functions and Flows for reads;
mutations are suspend. Paged lists are populated by RemoteMediators in the
sources module — e.g. a feed mediator and a chat-message mediator — which fetch
pages from the backend, write them through the DAO, and let Room/Paging serve the UI
from the local copy. This gives offline reads and a single source of truth.
Preferences and small caches use Jetpack DataStore rather than the database.
A representative example is
OpenGraphCacheProvider,
which caches link-preview metadata via PreferenceDataStoreFactory.create(...) with
a corruption handler and JSON-serialized values. User flags and similar small state
follow the same pattern.
Routing all storage access through the provider facade keeps Room out of feature
modules; per-user database files make account isolation structural rather than
something each query has to remember; and the RemoteMediator layer keeps the
network-to-cache sync in one place so screens just observe the database.