# ADR-0005: FFI Bridge via Flutter Rust Bridge v2 - **Status**: Accepted - **Date**: 2026-08-20 - **Task**: 1.5 — Generate flutter_rust_bridge FFI bindings bridging Rust storage methods to Dart ## Context Tasks 1.1–1.4 established the Rust core engine (`chronogrid_core` with SQLCipher + Automerge CRDT) and the cryptographic primitives (`chronogrid_crypto` with Argon2id KDF + OS keychain). The Flutter presentation layer needs a type-safe, zero-copy FFI bridge to invoke these Rust functions from Dart. The project's architectural mandate (`.clinerules`) requires: *"FFI Bridge: Use `flutter_rust_bridge` C-ABI generation ONLY."* ## Decision ### 1. New Crate: `chronogrid_ffi` A dedicated bridge crate (`crates/chronogrid_ffi/`) was created with: - **`[lib] crate-type = ["lib", "cdylib", "staticlib"]`** — produces a shared library for Flutter's FFI loader on all platforms, plus a static library for iOS. - **Dependencies**: `chronogrid_core`, `chronogrid_crypto`, `flutter_rust_bridge = "=2.7.0"`, `anyhow = "1"`. ### 2. API Surface Design — Serialized CRDT Bytes + Engine Singleton Rather than passing opaque Rust pointers across FFI, the API uses a **serialized-bytes pattern**: - **CRDT documents** are serialized to/from `Vec` (Automerge binary format). - **Mutation functions** accept `doc_bytes: Vec`, apply the edit, and return updated `Vec`. - **Read functions** (`task_crdt_read`, `note_crdt_read`) deserialize bytes into FFI-safe DTO structs (`FfiTaskData`, `FfiNoteData`). - **Merge functions** are stateless — they accept two document byte arrays and return the merged result. This avoids opaque pointer lifetime management, ensures Dart never holds stale Rust references, and leverages Automerge's natural serialization boundary. ### 3. Engine Singleton (`Mutex>`) A global `Engine` struct holds: - `db: Database` — the open SQLCipher connection. - `hlc: Hlc` — the node's Hybrid Logical Clock. The engine is stored in a `static Mutex>` with helper functions `with_engine()` and `with_engine_mut()` for safe concurrent access from FRB's thread pool. - `init_engine(path, key, node_id)` / `init_engine_in_memory(key, node_id)` — opens the database and seeds the HLC. - `close_engine()` — drops the connection. - `is_engine_initialized()` — status check. ### 4. Module Structure | Module | Functions | Category | |----------------|-----------|----------| | `engine` | 4 | Lifecycle (init/close/status) | | `database` | 3 | CRDT doc persistence (save/load/delete) | | `crypto` | 5 | Key derivation + keychain (platform-gated) | | `task_crdt` | 18 | Task CRDT create/read/set_*/clear_*/merge | | `note_crdt` | 8 | Note CRDT create/read/set_*/clear_*/merge | | `types` | 3 structs | `FfiDerivedKey`, `FfiTaskData`, `FfiNoteData` | **Total**: 38 public FFI functions + 3 DTO structs. ### 5. Error Handling All fallible functions return `anyhow::Result`, which FRB v2 automatically maps to Dart exceptions with the error message string. Internal `CoreError` and `CryptoError` variants are converted via their `Display` impls. ### 6. Platform Gating Keychain functions (`crypto_store_credential`, `crypto_retrieve_credential`, `crypto_delete_credential`) compile on all platforms but return an error on Android, where credential storage is delegated to the Flutter KeyStore bridge. ### 7. Code Generation Config `flutter_rust_bridge.yaml` at workspace root configures the codegen: - **Rust input**: `crates/chronogrid_ffi/src/api/` - **Rust output**: `crates/chronogrid_ffi/src/frb_generated.rs` - **Dart output**: `apps/chronogrid_flutter/lib/src/rust/` After running `flutter_rust_bridge_codegen generate`, the `frb_generated` module must be uncommented in `lib.rs`. ### 8. Flutter Side `pubspec.yaml` updated with `flutter_rust_bridge: ^2.7.0`, `ffi: ^2.1.0`, and `meta: ^1.11.0`. ## Testing - **25 unit tests** covering: engine lifecycle (init/close/double-init/bad-key), database CRDT doc persistence (save/load/delete/missing), task CRDT (create/read/set-all-fields/clear-optionals/merge/stateless-read/no-engine-error), note CRDT (create/read/set-all-fields/clear-optionals/merge/stateless-read/no-engine-error), and crypto (derive/deterministic/invalid-salt/generate-salt). - All 25 tests pass with `--test-threads=1` (required due to global engine singleton). - Verification pipeline (`scripts/verify_agent_work.ps1`) passes all checks. ## Consequences - The Dart presentation layer can invoke all core Rust functionality via generated type-safe bindings after a single `flutter_rust_bridge_codegen generate` run. - CRDT documents flow as opaque byte arrays across FFI, maintaining the local-first zero-cloud mandate. - The engine singleton pattern means only one database can be open at a time per process, which matches the single-user design. - Future phases can extend the API by adding new `pub fn` entries to the `api/` modules and re-running codegen.