# Database & Storage ## Table of Contents 1. [Overview](#1-overview) 2. [Database Files & Paths](#2-database-files--paths) 3. [Haskell Store Modules](#3-haskell-store-modules) 4. [Migrations](#4-migrations) 5. [Database Encryption](#5-database-encryption) 6. [File Storage](#6-file-storage) 7. [Export & Import](#7-export--import) 8. [Source Files](#8-source-files) --- ## 1. Overview SimpleX Chat uses **two SQLite databases** managed entirely by the Haskell core. Kotlin code **never reads or writes the databases directly** -- all data access goes through the JNI command/response protocol defined in [SimpleXAPI.kt](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt). The two databases are: | Database | Suffix | Contents | |----------|--------|----------| | Chat database | `_chat.db` | Users, contacts, groups, messages, files metadata, settings | | Agent database | `_agent.db` | SMP/XFTP agent state: connections, queues, encryption keys, delivery tracking | Both databases are created and migrated by the `chatMigrateInit` JNI function. The Kotlin layer handles: - Providing the correct file path prefix (`dbAbsolutePrefixPath`) - Providing the encryption key - Interpreting migration results (`DBMigrationResult`) - Exposing API functions that proxy to Haskell store operations --- ## 2. Database Files & Paths ### Expect Declarations The common module declares platform-dependent paths as `expect` values in [Files.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt): ```kotlin expect val dataDir: File // L18 expect val tmpDir: File // L19 expect val filesDir: File // L20 expect val appFilesDir: File // L21 expect val wallpapersDir: File // L22 expect val coreTmpDir: File // L23 expect val dbAbsolutePrefixPath: String // L24 expect val preferencesDir: File // L25 expect val preferencesTmpDir: File // L26 expect val chatDatabaseFileName: String // L28 expect val agentDatabaseFileName: String // L29 expect val databaseExportDir: File // L35 expect val remoteHostsDir: File // L37 ``` ### Android Actual Values From [Files.android.kt](../common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt): | Variable | Value | Notes | |----------|-------|-------| | `dataDir` | `androidAppContext.dataDir` | `/data/data//` | | `tmpDir` | `getDir("temp", MODE_PRIVATE)` | Private temp directory | | `filesDir` | `dataDir/files` | Parent for all file storage | | `appFilesDir` | `filesDir/app_files` | User-visible chat file attachments | | `wallpapersDir` | `filesDir/assets/wallpapers` | Custom wallpaper images | | `coreTmpDir` | `filesDir/temp_files` | Haskell core temp directory | | `dbAbsolutePrefixPath` | `dataDir/files` | Prefix: core appends `_chat.db` / `_agent.db` | | `chatDatabaseFileName` | `"files_chat.db"` | Full filename: `files_chat.db` | | `agentDatabaseFileName` | `"files_agent.db"` | Full filename: `files_agent.db` | | `databaseExportDir` | `androidAppContext.cacheDir` | Temp location for archive export | | `remoteHostsDir` | `tmpDir/remote_hosts` | Remote host file staging | | `preferencesDir` | `dataDir/shared_prefs` | Android SharedPreferences directory | ### Desktop Actual Values From [Files.desktop.kt](../common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt): | Variable | Value | Notes | |----------|-------|-------| | `dataDir` | `desktopPlatform.dataPath` | XDG_DATA_HOME (Linux), AppData (Windows), Application Support (macOS) | | `tmpDir` | `java.io.tmpdir/simplex` | System temp with `deleteOnExit` | | `filesDir` | `dataDir/simplex_v1_files` | Flat file storage | | `appFilesDir` | Same as `filesDir` | No subdirectory on desktop | | `wallpapersDir` | `dataDir/simplex_v1_assets/wallpapers` | Custom wallpaper images | | `coreTmpDir` | `dataDir/tmp` | Haskell core temp directory | | `dbAbsolutePrefixPath` | `dataDir/simplex_v1` | Prefix: core appends `_chat.db` / `_agent.db` | | `chatDatabaseFileName` | `"simplex_v1_chat.db"` | Full filename: `simplex_v1_chat.db` | | `agentDatabaseFileName` | `"simplex_v1_agent.db"` | Full filename: `simplex_v1_agent.db` | | `databaseExportDir` | Same as `tmpDir` | Temp location for archive export | | `remoteHostsDir` | `dataDir/remote_hosts` | Remote host file staging | | `preferencesDir` | `desktopPlatform.configPath` | Platform config directory | ### Resulting Database Paths | Platform | Chat DB | Agent DB | |----------|---------|----------| | Android | `/data/data//files_chat.db` | `/data/data//files_agent.db` | | Desktop (Linux) | `~/.local/share/simplex/simplex_v1_chat.db` | `~/.local/share/simplex/simplex_v1_agent.db` | | Desktop (macOS) | `~/Library/Application Support/simplex/simplex_v1_chat.db` | ... | | Desktop (Windows) | `%APPDATA%/simplex/simplex_v1_chat.db` | ... | --- ## 3. Haskell Store Modules The Haskell core organizes database access into store modules. Kotlin code invokes these indirectly through `CC` commands. The store modules are: | Module | Path | Responsibilities | |--------|------|-----------------| | `Messages.hs` | `src/Simplex/Chat/Store/Messages.hs` | Message CRUD, chat items, reactions, delivery statuses, TTL cleanup | | `Groups.hs` | `src/Simplex/Chat/Store/Groups.hs` | Group profiles, membership, roles, invitations, group links | | `Direct.hs` | `src/Simplex/Chat/Store/Direct.hs` | Contact management, direct connections, contact requests | | `Files.hs` | `src/Simplex/Chat/Store/Files.hs` | File transfer metadata, XFTP state, standalone files | | `Profiles.hs` | `src/Simplex/Chat/Store/Profiles.hs` | User profiles, display names, address book | | `Connections.hs` | `src/Simplex/Chat/Store/Connections.hs` | SMP agent connections, pending connections, server switches | All store operations execute within SQLite transactions managed by the Haskell core. The Kotlin layer has no direct knowledge of table schemas or SQL queries. --- ## 4. Migrations ### JNI Entry Point Database migration is triggered by the `chatMigrateInit` external function ([Core.kt#L25](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L25)): ```kotlin external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array ``` **Parameters:** - `dbPath` -- the `dbAbsolutePrefixPath` (core appends `_chat.db` and `_agent.db`) - `dbKey` -- encryption passphrase (empty string = unencrypted) - `confirm` -- migration confirmation mode: `"error"`, `"yesUp"`, or `"yesUpDown"` **Returns:** `Array` where: - `[0]` -- JSON string encoding a `DBMigrationResult` - `[1]` -- `ChatCtrl` handle (Long) if migration succeeded ### Migration Flow in `initChatController` The full initialization sequence is in [Core.kt#L62](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L62): 1. Obtain the DB encryption key from `DatabaseUtils.useDatabaseKey()`. 2. Determine the confirmation mode (default: `YesUp`; developer mode with confirm upgrades: `Error`). 3. Call `chatMigrateInit(dbAbsolutePrefixPath, dbKey, "error")` -- first attempt with `Error` to detect pending migrations. 4. Parse the result as `DBMigrationResult`. 5. If the result is `ErrorMigration` with an `Upgrade` error and confirmation allows it, re-run `chatMigrateInit` with the appropriate confirmation (`"yesUp"`). 6. If `OK`, store the `ChatCtrl` handle, set `chatDbEncrypted`, and proceed to start the chat. 7. If not `OK`, handle special case: if the `newDatabaseInitialized` preference is not set AND the database was only partially initialized (single DB file exists), remove both files and retry once. ### DBMigrationResult Defined in [DatabaseUtils.kt#L79](../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt#L79): ```kotlin sealed class DBMigrationResult { object OK // Migration succeeded object InvalidConfirmation // Invalid confirmation parameter data class ErrorNotADatabase(val dbFile: String) // File exists but is not a valid database data class ErrorMigration(val dbFile: String, // Migration error with details val migrationError: MigrationError) data class ErrorSQL(val dbFile: String, // SQL error during migration val migrationSQLError: String) object ErrorKeychain // Keychain/keystore error data class Unknown(val json: String) // Unparseable response } ``` ### MigrationError ```kotlin sealed class MigrationError { class Upgrade(val upMigrations: List) // Pending forward migrations class Downgrade(val downMigrations: List) // Database is newer than app class Error(val mtrError: MTRError) // Conflict or missing migrations } ``` ### MigrationConfirmation ```kotlin enum class MigrationConfirmation(val value: String) { YesUp("yesUp"), // Auto-confirm forward migrations YesUpDown("yesUpDown"), // Auto-confirm both directions (not used in UI) Error("error") // Report errors without running migrations } ``` --- ## 5. Database Encryption ### Encryption API Two API functions manage database encryption, both in [SimpleXAPI.kt](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt): | Function | Parameters | Description | Line | |----------|-----------|-------------|------| | `apiStorageEncryption` | `currentKey: String, newKey: String` | Change or set the database encryption passphrase | [L999](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L999) | | `testStorageEncryption` | `key: String, ctrl: ChatCtrl?` | Test whether a given key can decrypt the database | [L1006](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1006) | Both delegate to the Haskell core via `CC.ApiStorageEncryption(DBEncryptionConfig)` and `CC.TestStorageEncryption(key)` respectively. `DBEncryptionConfig` ([SimpleXAPI.kt#L4166](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L4166)): ```kotlin class DBEncryptionConfig(val currentKey: String, val newKey: String) ``` ### Passphrase Storage -- CryptorInterface The `CryptorInterface` ([Cryptor.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt)) provides platform-specific key encryption for storing the DB passphrase at rest: ```kotlin interface CryptorInterface { fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? fun encryptText(text: String, alias: String): Pair fun deleteKey(alias: String) } expect val cryptor: CryptorInterface ``` ### Android Implementation [Cryptor.android.kt](../common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt): - Uses **Android KeyStore** (`"AndroidKeyStore"` provider) - Algorithm: **AES/GCM/NoPadding** (128-bit authentication tag) - Keys are hardware-backed when available - On decryption failure with a random initial passphrase, throws to prevent overwriting - Shows user alerts for keychain errors ```kotlin internal class Cryptor: CryptorInterface { private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } // AES-GCM encryption/decryption using AndroidKeyStore-managed keys } ``` ### Desktop Implementation [Cryptor.desktop.kt](../common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt): - **Placeholder/no-op implementation** -- data is returned as-is - No actual encryption of the stored passphrase on desktop - `decryptData` returns `String(data)` without decryption - `encryptText` returns the raw bytes without encryption ```kotlin actual val cryptor: CryptorInterface = object : CryptorInterface { override fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? = String(data) override fun encryptText(text: String, alias: String) = text.toByteArray() to text.toByteArray() override fun deleteKey(alias: String) {} } ``` ### Passphrase Management `DatabaseUtils` ([DatabaseUtils.kt](../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt)) provides: - `ksDatabasePassword` -- encrypted passphrase stored in platform preferences (SharedPreferences on Android, file-based on desktop) - `useDatabaseKey()` -- retrieves the passphrase, decrypting it via `CryptorInterface` - `randomDatabasePassword()` -- generates a 32-byte random passphrase (Base64-encoded) for initial database creation The flow: 1. On first launch, `randomDatabasePassword()` generates a key. 2. `CryptorInterface.encryptText()` encrypts the key for storage. 3. The encrypted (data, IV) pair is saved to preferences via `ksDatabasePassword`. 4. On subsequent launches, `ksDatabasePassword.get()` retrieves the encrypted pair, and `CryptorInterface.decryptData()` recovers the plaintext key. 5. The key is passed to `chatMigrateInit` to open the encrypted SQLite databases. --- ## 6. File Storage ### Directory Layout Declared in [Files.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) with platform-specific implementations: | Directory | Variable | Android Path | Desktop Path | Purpose | |-----------|----------|-------------|--------------|---------| | App files | `appFilesDir` | `dataDir/files/app_files` | `dataDir/simplex_v1_files` | Chat file attachments (images, videos, documents) | | Wallpapers | `wallpapersDir` | `dataDir/files/assets/wallpapers` | `dataDir/simplex_v1_assets/wallpapers` | Custom chat wallpaper images | | Core temp | `coreTmpDir` | `dataDir/files/temp_files` | `dataDir/tmp` | Haskell core temporary files (in-progress transfers) | | App temp | `tmpDir` | `getDir("temp", MODE_PRIVATE)` | `java.io.tmpdir/simplex` | Application-level temporary files | | Remote hosts | `remoteHostsDir` | `tmpDir/remote_hosts` | `dataDir/remote_hosts` | Files staged for remote host sessions | | DB export | `databaseExportDir` | `androidAppContext.cacheDir` | Same as `tmpDir` | Temporary storage for database archive ZIP | | Preferences | `preferencesDir` | `dataDir/shared_prefs` | `desktopPlatform.configPath` | User preferences, theme YAML | | Migration temp | `getMigrationTempFilesDirectory()` | `dataDir/migration_temp_files` | `dataDir/migration_temp_files` | Temporary files during database migration | ### File Path Resolution Files referenced by chat items use `CryptoFile` (optional encryption metadata + relative path). Path resolution is handled by helper functions in [Files.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt): - `getAppFilePath(fileName)` ([L81](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L81)) -- resolves to `appFilesDir/fileName` for local, or `remoteHostsDir//simplex_v1_files/fileName` for remote hosts - `getWallpaperFilePath(fileName)` ([L91](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L91)) -- resolves wallpaper paths similarly - `getLoadedFilePath(file)` ([L105](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L105)) -- returns the full path if the file is downloaded and ready ### Local File Encryption The `apiSetEncryptLocalFiles(enable)` command ([SimpleXAPI.kt#L967](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L967)) tells the Haskell core to encrypt files stored in `appFilesDir`. When enabled, files are written as `CryptoFile` with a random AES key and nonce. The JNI functions `chatEncryptFile` and `chatDecryptFile` ([Core.kt#L39-L40](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L39)) handle the actual crypto operations. --- ## 7. Export & Import ### API Functions | Function | CC Command | CR Response | Line | |----------|-----------|-------------|------| | `apiExportArchive(config)` | `CC.ApiExportArchive(config)` | `CR.ArchiveExported(archiveErrors)` | [SimpleXAPI.kt#L981](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L981) | | `apiImportArchive(config)` | `CC.ApiImportArchive(config)` | `CR.ArchiveImported(archiveErrors)` | [SimpleXAPI.kt#L987](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L987) | | `apiDeleteStorage()` | `CC.ApiDeleteStorage()` | `CR.CmdOk` | [SimpleXAPI.kt#L993](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L993) | ### ArchiveConfig Defined at [SimpleXAPI.kt#L4162](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L4162): ```kotlin class ArchiveConfig( val archivePath: String, // Full path to the ZIP archive val disableCompression: Boolean?, // Skip compression for speed val parentTempDirectory: String? // Temp directory for extraction ) ``` ### Export Flow 1. UI constructs an `ArchiveConfig` with a path under `databaseExportDir`. 2. Calls `apiExportArchive(config)` which sends `CC.ApiExportArchive` to the Haskell core. 3. The core creates a ZIP containing both `_chat.db` and `_agent.db` (and optionally files). 4. Returns `CR.ArchiveExported` with a list of `ArchiveError` (non-fatal issues during export). 5. UI offers the archive file for sharing/saving. ### Import Flow 1. User selects an archive file. 2. UI copies it to a temp location and constructs an `ArchiveConfig`. 3. Calls `apiImportArchive(config)` which sends `CC.ApiImportArchive` to the Haskell core. 4. The core extracts and replaces both databases. 5. Returns `CR.ArchiveImported` with a list of `ArchiveError` (non-fatal issues during import). 6. UI triggers re-initialization via `initChatController`. ### ArchiveError Defined at [SimpleXAPI.kt#L7658](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7658): ```kotlin sealed class ArchiveError { class ArchiveErrorImport(val importError: String) // General import error class ArchiveErrorFile(val file: String, val fileError: String) // Per-file error } ``` ### Delete Storage `apiDeleteStorage()` removes both database files entirely. This is used during account deletion or database reset operations. After calling this, `initChatController` must be called to create fresh databases. --- ## 8. Source Files | File | Purpose | Path | |------|---------|------| | SimpleXAPI.kt | API functions: encryption, export/import, storage commands | `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` | | Core.kt | JNI externals (`chatMigrateInit`, `chatEncryptFile`, etc.), `initChatController` | `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` | | Files.kt | Platform-expect file/directory path declarations | `common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt` | | Files.android.kt | Android actual paths (dataDir, appFilesDir, etc.) | `common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt` | | Files.desktop.kt | Desktop actual paths (XDG/AppData, etc.) | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt` | | Cryptor.kt | Platform-expect encryption interface for passphrase storage | `common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt` | | Cryptor.android.kt | Android: AES-GCM via AndroidKeyStore | `common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt` | | Cryptor.desktop.kt | Desktop: placeholder (no-op) implementation | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt` | | DatabaseUtils.kt | `DBMigrationResult`, `MigrationError`, `MigrationConfirmation`, passphrase helpers | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` | | Messages.hs | Haskell store: message CRUD, reactions, delivery | `src/Simplex/Chat/Store/Messages.hs` | | Groups.hs | Haskell store: groups, membership, roles | `src/Simplex/Chat/Store/Groups.hs` | | Direct.hs | Haskell store: contacts, direct connections | `src/Simplex/Chat/Store/Direct.hs` | | Files.hs | Haskell store: file transfer metadata | `src/Simplex/Chat/Store/Files.hs` | | Profiles.hs | Haskell store: user profiles | `src/Simplex/Chat/Store/Profiles.hs` | | Connections.hs | Haskell store: SMP agent connections | `src/Simplex/Chat/Store/Connections.hs` | All Kotlin paths are relative to `apps/multiplatform/`. All Haskell paths are relative to the repository root.