| name | frb-patterns |
| description | Flutter Rust Bridge patterns and best practices for this project. Use when writing Rust API code, adding new bindings, implementing MlsEngine methods, or troubleshooting FRB issues. |
FRB Patterns for openmls
Patterns and templates for writing correct Flutter Rust Bridge code in this project.
Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ OpenMLS (Rust crate) โ Core MLS implementation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ EncryptedDb (SQLCipher / IDB+WebCrypto) โ Platform-specific encrypted KV store
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ SnapshotStorageProvider (HashMap) โ In-memory OpenMLS StorageProvider
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ MlsEngine (rust/src/api/engine.rs) โ FRB-annotated async methods
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ lib/src/rust/*.dart (FRB generated) โ Auto-generated Dart API
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Your Dart application code โ Uses MlsEngine
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key Components
| Component | File | Purpose |
|---|
| MlsEngine | rust/src/api/engine.rs | Opaque FRB struct โ all MLS operations as async methods on &self |
| EncryptedDb | rust/src/encrypted_db.rs | SQLCipher (native) / IndexedDB + AES-256-GCM via Web Crypto (WASM) |
| SnapshotStorageProvider | rust/src/snapshot_storage.rs | HashMap-based StorageProvider loaded from EncryptedDb snapshots |
| SnapshotOpenMlsProvider | rust/src/snapshot_storage.rs | Combines RustCrypto + SnapshotStorageProvider |
MlsEngine Pattern
MlsEngine is an opaque FRB struct that owns an EncryptedDb. All MLS operations are async methods on &self:
pub struct MlsEngine {
db: EncryptedDb,
}
impl MlsEngine {
pub async fn create(db_path: String, encryption_key: Vec<u8>) -> Result<MlsEngine, String> {
let db = EncryptedDb::open(db_path, encryption_key).await?;
Ok(MlsEngine { db })
}
pub async fn create_group(
&self,
config: MlsGroupConfig,
signer_bytes: Vec<u8>,
credential_identity: Vec<u8>,
signer_public_key: Vec<u8>,
group_id: Option<Vec<u8>>,
credential_bytes: Option<Vec<u8>>,
) -> Result<CreateGroupResult, String> {
let provider = self.load_for_group().await?;
self.commit(provider, Some(group_id_slice)).await?;
Ok(CreateGroupResult { group_id: })
}
}
Internal Helpers (load / commit cycle)
Every method that accesses MLS state follows this pattern:
let provider = self.load_for_group(group_id).await?;
let provider = self.load_global().await?;
let mut group = load_group(group_id, &provider)?;
group.add_members()?;
self.commit(provider, Some(group_id)).await?;
Adding a New API Function
- Add
pub async fn method on impl MlsEngine in rust/src/api/engine.rs
- Load the appropriate scope (
load_for_group or load_global)
- Perform OpenMLS operations
- Call
self.commit(provider, group_id) to persist changes
- Return a result struct (not opaque)
- Run
make codegen to generate Dart bindings
Example:
pub async fn my_new_function(
&self,
group_id_bytes: Vec<u8>,
signer_bytes: Vec<u8>,
) -> Result<Vec<u8>, String> {
let provider = self.load_for_group(&group_id_bytes).await?;
let signer = signer_from_bytes(signer_bytes)?;
let mut group = load_group(&group_id_bytes, &provider)?;
let result = ;
self.commit(provider, Some(&group_id_bytes)).await?;
Ok(result)
}
Dart usage (auto-generated by FRB):
final result = await engine.myNewFunction(
groupIdBytes: groupId,
signerBytes: signerBytes,
);
SnapshotStorageProvider (Sync Storage)
OpenMLS StorageProvider trait methods are synchronous. The SnapshotStorageProvider wraps an in-memory HashMap<Vec<u8>, Vec<u8>> โ no async bridging needed.
Key Format
Composite keys match OpenMLS MemoryStorage format:
fn build_key<const V: u16>(label: &[u8], key: &[u8]) -> Vec<u8> {
let mut out = label.to_vec();
out.extend_from_slice(key);
out.extend_from_slice(&u16::to_be_bytes(V));
out
}
Storage Diff
After OpenMLS operations, into_updates() diffs the initial snapshot vs current state to produce StorageUpdates (upserts + deletes) for persistence to EncryptedDb.
Opaque Type Pattern
For types that stay in Rust (not serialized across FFI):
#[frb(opaque)]
pub struct MlsSignatureKeyPair {
pub(crate) native: openmls_basic_credential::SignatureKeyPair,
}
impl MlsSignatureKeyPair {
#[flutter_rust_bridge::frb(sync)]
pub fn generate(ciphersuite: MlsCiphersuite) -> Result<MlsSignatureKeyPair, String> {
}
#[flutter_rust_bridge::frb(sync)]
pub fn serialize(&self) -> Vec<u8> {
}
}
Dart usage:
final signer = MlsSignatureKeyPair.generate(ciphersuite: ciphersuite);
final bytes = signer.serialize();
final pubKey = signer.publicKey();
Transparent Struct Pattern
For result/config types that cross FFI as plain data:
pub struct CreateGroupResult {
pub group_id: Vec<u8>,
}
pub struct MlsGroupConfig {
pub ciphersuite: MlsCiphersuite,
pub wire_format_policy: MlsWireFormatPolicy,
pub use_ratchet_tree_extension: bool,
}
FRB generates Dart classes with constructors for these automatically.
Sync vs Async Functions
Async (all MlsEngine methods)
pub async fn create_group(&self, ...) -> Result<CreateGroupResult, String> { ... }
Sync (simple operations, no DB access)
impl MlsSignatureKeyPair {
#[flutter_rust_bridge::frb(sync)]
pub fn serialize(&self) -> Vec<u8> { ... }
}
pub fn mls_message_extract_group_id(message_bytes: Vec<u8>) -> Result<Vec<u8>, String> { ... }
pub fn mls_message_content_type(message_bytes: Vec<u8>) -> Result<String, String> { ... }
Error Handling
Convert OpenMLS errors to String for FRB:
pub async fn some_function(&self, ...) -> Result<SomeResult, String> {
let group = MlsGroup::new(provider, &signer, &config, credential)
.map_err(|e| format!("Failed to create group: {e}"))?;
Ok(SomeResult { ... })
}
FRB automatically converts Result<T, String> to Dart exceptions.
Vec for Serialization
All serialized data crosses FFI as Vec<u8> / List<int> / Uint8List:
let bytes = group_id.as_slice().to_vec();
let group_id = GroupId::from_slice(&group_id_bytes);
Memory Management
FRB handles cleanup automatically via Rust's ownership system.
- No manual
dispose() needed in Dart
- No finalizers to register
- No double-free concerns
- Opaque types (MlsEngine, MlsSignatureKeyPair) are dropped when Dart GC collects them
// Dart โ no cleanup needed!
final engine = await MlsEngine.create(dbPath: ':memory:', encryptionKey: key);
final signer = MlsSignatureKeyPair.generate(ciphersuite: ciphersuite);
// Both automatically cleaned up when no longer referenced
Regenerating Bindings
After modifying Rust code in rust/src/api/:
make codegen
This runs flutter_rust_bridge_codegen generate using flutter_rust_bridge.yaml config.
When to regenerate:
- After modifying any
pub fn or pub async fn in rust/src/api/
- After changing struct/enum definitions in
rust/src/api/types.rs
- After updating OpenMLS version (if API changed)
Files Reference
| Pattern | Reference File |
|---|
| Engine API (all MLS methods) | rust/src/api/engine.rs |
| Encrypted storage | rust/src/encrypted_db.rs |
| Snapshot storage provider | rust/src/snapshot_storage.rs |
| Opaque types | rust/src/api/keys.rs |
| Transparent structs | rust/src/api/types.rs |
| Config types | rust/src/api/config.rs |
| Credential types | rust/src/api/credential.rs |
Common Issues
"method not found" after codegen
- Check that the method is
pub
- Check that return types are supported by FRB
- Run
make codegen after any Rust changes
Type not transferable
Use Vec<u8> for complex types instead of trying to pass OpenMLS types directly across FFI.
EncryptedDb errors on WASM
- Ensure the browser supports
crypto.subtle (HTTPS or localhost required)
- IndexedDB names must not collide between instances
- Pre-encrypt values before opening IDB transactions (crypto.subtle is async, IDB auto-commits on idle)
Web/WASM Considerations