Group Management and Federation
Group Creation Process
Section titled “Group Creation Process”fn create_group( founder: &DeviceIdentity, initial_members: &[LocalContact], group_name: String, provider: &impl OpenMlsProvider,) -> Result<MultiUserGroup> {
// 1. Generate random group ID let group_id = Uuid::new_v4();
// 2. Create permission extension let mut device_permissions = HashMap::new(); device_permissions.insert( founder.device_id, Permissions::ADMINISTRATOR );
let perm_ext = CryptidPermissionExtension { device_permissions, founder: founder.device_id, version: 0, last_updated_at: current_unix_timestamp(), last_updated_by: founder.device_id, };
// 3. Create group metadata extension let metadata_ext = CryptidGroupMetadata { group_name, group_picture: None, group_description: None, threads: HashMap::new(), version: 0, last_updated_at: current_unix_timestamp(), last_updated_by: founder.device_id, };
// 4. Serialize both extensions let perm_data = serde_json::to_vec(&perm_ext)?; let metadata_data = serde_json::to_vec(&metadata_ext)?;
let perm_extension = Extension::new( ExtensionType::Unknown(CRYPTID_PERMISSIONS_EXT), extension_data.into() );
let metadata_extension = Extension::new( ExtensionType::Unknown(CRYPTID_GROUP_METADATA_EXT), extension_data.into() );
// 5. Create MLS group with founder's credential, RatchetTreeExtension, and Extensions let mut extensions = Extensions::empty(); extensions.add(perm_extension)?; extensions.add(metadata_extension)?;
let config = MlsGroupCreateConfig::builder() .with_ratchet_tree_extension(true) .with_group_context_extensions(extensions)? .build();
let mut mls_group = MlsGroup::new( provider, &founder.keypair, &config, founder.create_mls_credential(), )?;
// 6. Fetch KeyPackages for ALL devices of each user let mut all_keypackages = Vec::new(); let mut device_to_user_map = HashMap::new();
for user_contact in initial_members { // Fetch KeyPackages for all of this user's devices for device in &user_contact.devices { match fetch_keypackage(&device.keypackage_server, &device.device_id) { Ok(keypackage) => { all_keypackages.push((device.clone(), keypackage)); device_to_user_map.insert(device.device_id, user_contact.user_id); } Err(e) => { log::warn!("Failed to fetch KeyPackage for device {}: {}", hex::encode(&device.device_id), e); // Continue with other devices (partial addition) } } } }
if all_keypackages.is_empty() { return Err("No KeyPackages available for any invited user"); }
// Extract just the KeyPackages for MLS let keypackage: Vec<KeyPackage> = all_keypackages.iter() .map(|(_, kp)| kp.clone()) .collect();
// 7. Add all devices in one operation (OpenMLS API) let (commit, welcomes) = mls_group.add_members( provider, &founder.keypair, &keypackages, )?;
// Commit the changes mls_group.merge_pending_commit(provider)?;
// 8. Update permission extension to include new members // (Give them default member permissions) let mut updated_perm_ext = perm_ext.clone(); for (device, _) in &all_keypackages { updated_perm_ext.device_permissions.insert( device.device_id, Permissions::default_member() ); } updated_perm_ext.version += 1;
// Create updated extension let updated_extension_data = serde_json::to_vec(&updated_perm_ext)?; let updated_extension = Extension::new( ExtensionType::Unknown(CRYPTID_PERMISSIONS_EXT), updated_extension_data.into() );
// Update group context with new permissions let (perm_commit, _) = mls_group.update_group_context_extensions( provider, Extensions::single(updated_extension), &founder.signer, )?; mls_group.merge_pending_commit(provider)?;
// 9. Send Welcome messages to all added devices for ((device, _), welcome) in all_keypackages.iter().zip(welcomes) { send_message( welcome, &device.initial_delivery_address, )?; }
// 10. Build member list (users, not individual devices) let members = build_member_list_from_mls(&mls_group, &device_to_user_map);
Ok(MultiUserGroup { group_id, founder_device: founder.device_id, members, device_to_user: device_to_user_map, mls_state: mls_group, created_at: current_unix_timestamp(), })
}Adding Members to Groups
Section titled “Adding Members to Groups”Members can add people they have as contacts. When adding a member, KeyPackages for all their devices must be fetched from their designated server.
Multi-Device Invitation:
When inviting a user to a group:
- Fetch KeyPackages for ALL of the user’s devices
- Add all devices to the MLS group in a single commit
- Send Welcome messages to each device’s delivery address
- Handle partial failures (some devices may not have KeyPackages available)
See Contact Exchange and Trust for details on KeyPackage management.
fn add_member_to_group( group: &mut MultiUserGroup, user_contact: &LocalContact, // Represents a User inviting_device: &DeviceIdentity, provider: &impl OpenMlsProvider,) -> Result<()> { // 1. Check permissions from MLS group extension let perm_ext = group.get_permission_extension()?; let inviter_perms = perm_ext.device_permissions .get(&inviting_device.device_id) .ok_or("Inviting device not in group")?;
if !inviter_perms.contains(Permissions::INVITE_MEMBERS) && !inviter_perms.contains(Permissions::ADMINISTRATOR) { return Err("Insufficient permissions to invite new members".into()); }
// 2. Fetch KeyPackages for ALL of the user's devices let mut successful_devices = Vec::new(); let mut failed_devices = Vec::new();
for device in &user_contact.devices { match fetch_keypackage(&device.keypackage_server, &device.deviceid) { Ok(keypackage) => { successful_devices.push((device.clone(), keypackage)); } Err(e) => { failed_devices.push((device.deviceid, e.to_string())); log::warn!("Failed to fetch KeyPackage for device {}: {}", hex::encode(&device.deviceid), e); } } }
// 3. Proceed if at least ONE device has a KeyPackage if successful_devices.is_empty() { return Err("No KeyPackages available for any of the user's devices"); }
// Extract KeyPackages for MLS let keypackages: Vec<KeyPackage> = successful_devices.iter() .map(|(_, kp)| kp.clone()) .collect();
// 4. Add all successful devices to MLS group in a single commit let (commit, welcomes) = group.mls_state.add_members( provider, &inviting_device.keypair, &keypackages, )?;
// 5. Merge pending commit to update local group state group.mls_state.merge_pending_commit(provider)?;
// 6. Update permission extension to grant default permissions to new devices let mut perm_ext = group.get_permission_extension()?; for (device, _) in &successful_devices { perm_ext.device_permissions.insert( device.device_id, Permissions::default_member() ); } perm_ext.version += 1; perm_ext.last_updated_at = current_unix_timestamp(); perm_ext.last_updated_by = inviting_device.device_id;
// Update group context with new permissions let extension_data = serde_json::to_vec(&perm_ext)?; let extension = Extension::new( ExtensionType::Unknown(CRYPTID_PERMISSIONS_EXT), extension_data.into() );
let (perm_commit, _) = group.mls_state.update_group_context_extensions( provider, Extensions::single(extension), &inviting_device.signer, )?; group.mls_state.merge_pending_commit(provider)?;
// 7. Serialize commit for distribution let commit_message = commit.tls_serialize_detached()?; let perm_commit_message = perm_commit.tls_serialize_detached()?;
// 8. Send commits to all current members (updates their group state) for member in &group.members { // Get all devices for this member for device_id in &member.devices_in_group { // Send both the add commit and permission update commit for msg in [&commit_message, &perm_commit_message] { let secure_message = SecureMessage { message_id: Uuid::now_v7(), group_id: group.group_id, mls_ciphertext: msg.clone(), sender_signature: inviting_device.sign(msg), timestamp: current_unix_timestamp(), message_type: MessageType::SystemOperation, };
// Send to device's delivery address if let Some(device_info) = find_device_info(device_id) { send_message( secure_message, &device_info.initial_delivery_address, )?; } } } }
// 9. Send Welcome to each successfully added device for ((device, _), welcome) in successful_devices.iter().zip(welcomes) { let welcome_message = welcome.tls_serialize_detached()?; send_message(welcome_message, &device.initial_delivery_address)?; }
// 10. Update local group state // Check if user is already a member (adding new devices to existing user) if let Some(existing_member) = group.members.iter_mut() .find(|m| m.user_id == user_contact.user_id) { // Add new devices to existing member for (device, _) in &successful_devices { if !existing_member.devices_in_group.contains(&device.device_id) { existing_member.devices_in_group.insert(device.device_id); group.device_to_user.insert(device.device_id, user_contact.user_id); } } } else { // Add as new member let device_ids: HashSet<_> = successful_devices.iter() .map(|(d, _)| d.device_id) .collect();
for device_id in &device_ids { group.device_to_user.insert(*device_id, user_contact.user_id); }
group.members.push(GroupMember { user_id: user_contact.user_id, default_persona: user_contact.default_persona.clone(), personas: user_contact.personas.clone(),
devices_in_group: device_ids,
devices_pending: failed_devices.iter() .filter_map(|(device_id, _)| { user_contact.devices.iter() .find(|d| &d.device_id == device_id) .cloned() }) .collect(),
added_by: inviting_device.device_id, added_at: current_unix_timestamp(), }); }
Ok(InvitationResult { invited_user: user_contact.user_id, successful_devices: successful_devices.iter() .map(|(d, _)| d.device_id) .collect(), failed_devices, })}
struct InvitationResult { invited_user: UserId, successful_devices: Vec<DeviceId>, // device_ids that were added failed_devices: Vec<(DeviceId, String)>, // device_ids that failed + reason}Group Member Structure
Section titled “Group Member Structure”The GroupMember struct now represents users (not individual devices):
struct GroupMember { // User identity user_id: UserId,
// Personas/identities for this user // See the Crypitd's Identity System doc for more info default_persona: Persona, personas: Vec<Persona>,
// Device membership tracking devices_in_group: HashSet<DeviceId>, // Device IDs devices_pending: Vec<DevicePublicInfo>, // Failed during initial invite
// Who invited this member to the group added_by: DeviceId, added_at: u64,}
struct MultiUserGroup { // Cryptographically random group_id: GroupId, // Who created the group (device-level) founder_device: DeviceId,
// Members are users, not devices members: Vec<GroupMember>,
// Fast lookup: device_id -> user_id device_to_user: HashMap<DeviceId, UserId>,
// Current MLS epoch and keys (contains permission extension) mls_state: MlsGroup, created_at: u64,}
impl MultiUserGroup { /// Extract permissions from MLS group context fn get_permission_extension(&self) -> Result<CryptidPermissionExtension> { let extensions = self.mls_state.group_context().extensions();
let perm_ext = extensions .iter() .find(|ext| matches!( ext.extension_type(), ExtensionType::Unknown(CRYPTID_PERMISSIONS_EXT) )) .ok_or("Permission extension missing from group")?;
let permissions: CryptidPermissionExtension = serde_json::from_slice(perm_ext.extension_data())?;
Ok(permissions) }
/// Check if device has specific permission fn has_permission( &self, device_id: &DeviceId, perm: Permissions ) -> Result<bool> { let perm_ext = self.get_permission_extension()?;
if let Some(device_perms) = perm_ext.device_permissions.get(device_id) { Ok(device_perms.contains(perm)) } else { Ok(false) } }
/// Fast lookup to get user info from device_id fn get_user_for_device(&self, device_id: &DeviceId) -> Option<&GroupMember> { let user_id = self.device_to_user.get(device_id)?; self.members.iter().find(|m| &m.user_id == user_id) }}Why track devices within users?
- UI displays “Alice (3 devices in group, 2 pending)”
- Enables per-device operations (e.g., removing a compromising device)
- Maintains user-centric view while preserving device-level MLS membership
Handling Failed Devices
Section titled “Handling Failed Devices”Users can retry adding devices that failed during initial invitation:
fn add_pending_devices_to_group( group: &mut MultiUserGroup, member_user_id: &UserId, inviting_device: &DeviceIdentity, provider: &impl OpenMlsProvider,) -> Result<Vec<DeviceId>> { // 1. Check permissions let perm_ext = group.get_permission_extension()?; let inviter_perms = perm_ext.device_permissions .get(&inviting_device.device_id) .ok_or("Inviting device not in group")?;
if !inviter_perms.contains(Permissions::INVITE_MEMBERS) && !inviter_perms.contains(Permissions::ADMINISTRATOR) { return Err("Insufficient permissions to add devices".into()); }
// 2. Find member let member = group.members.iter_mut() .find(|m| &m.user_id == member_user_id) .ok_or("User not in group")?;
let mut added_devices = Vec::new();
// 3. Try to add each pending device for pending_device in &member.devices_pending.clone() { match fetch_keypackage(&pending_device.keypackage_server, &pending_device.deviceid) { Ok(keypackage) => { // Add this single device let (commit, welcome) = group.mls_state.add_members( provider, &inviting_device.keypair, &[keypackage], )?;
group.mls_state.merge_pending_commit(provider)?;
// Update permissions for the new device let mut perm_ext = group.get_permission_extension()?; perm_ext.device_permissions.insert( pending_device.device_id, Permissions::default_member() ); perm_ext.version += 1;
let extension_data = serde_json::to_vec(&perm_ext)?; let extension = Extension::new( ExtensionType::Unknown(CRYPTID_PERMISSIONS_EXT), extension_data.into() );
let (perm_commit, _) = group.mls_state.update_group_context_extensions( provider, Extensions::single(extension), &inviting_device.signer, )?; group.mls_state.merge_pending_commit(provider)?;
send_message(welcome, &pending_device.initial_delivery_address)?;
added_devices.push(pending_device.deviceid); member.devices_in_group.insert(pending_device.deviceid); group.device_to_user.insert(pending_device.device_id, *member_user_id); } Err(_) => continue, // Still no KeyPackage, leave pending } }
// Remove successfully added devices from pending list member.devices_pending.retain(|d| !added_devices.contains(&d.deviceid));
Ok(added_devices)}Permission Management
Section titled “Permission Management”Granting Permissions
Section titled “Granting Permissions”Devices with MANAGE_ROLES or ADMINISTRATOR permission can grant permissions to other group members:
async fn grant_permission_to_member( group: &mut MultiUserGroup, target_device: DeviceId, new_permissions: Permissions, granting_device: &DeviceIdentity, provider: &impl OpenMlsProvider,) -> Result<()> { // 1. Verify granting device has authority let perm_ext = group.get_permission_extension()?; let granter_perms = perm_ext.device_permissions .get(&granting_device.device_id) .ok_or("Granting device not in group")?;
if !granter_perms.contains(Permissions::MANAGE_ROLES) && !granter_perms.contains(Permissions::ADMINISTRATOR) { return Err("Insufficient permissions to manage roles".into()); }
// 2. Update permission extension let mut updated_perm_ext = perm_ext.clone(); updated_perm_ext.device_permissions.insert(target_device, new_permissions); updated_perm_ext.version += 1; updated_perm_ext.last_updated_at = current_unix_timestamp(); updated_perm_ext.last_updated_by = granting_device.device_id;
// 3. Create MLS commit with updated permissions let extension_data = serde_json::to_vec(&updated_perm_ext)?; let extension = Extension::new( ExtensionType::Unknown(CRYPTID_PERMISSIONS_EXT), extension_data.into() );
let (commit, _) = group.mls_state.update_group_context_extensions( provider, Extensions::single(extension), &granting_device.signer, )?;
// 4. Merge and distribute group.mls_state.merge_pending_commit(provider)?;
let commit_message = commit.tls_serialize_detached()?; for member in &group.members { for device_id in &member.devices_in_group { let secure_message = SecureMessage { message_id: Uuid::now_v7(), group_id: group.group_id, mls_ciphertext: commit_message.clone(), sender_signature: granting_device.sign(&commit_message), timestamp: current_unix_timestamp(), message_type: MessageType::SystemOperation, };
if let Some(device_info) = find_device_info(device_id) { send_message(secure_message, &device_info.initial_delivery_address)?; } } }
Ok(())}Revoking Permissions
Section titled “Revoking Permissions”async fn revoke_permission_from_member( group: &mut MultiUserGroup, target_device: DeviceId, permission_to_revoke: Permissions, revoking_device: &DeviceIdentity, provider: &impl OpenMlsProvider,) -> Result<()> { // Get current permissions let mut perm_ext = group.get_permission_extension()?;
// Verify authority let revoker_perms = perm_ext.device_permissions .get(&revoking_device.device_id) .ok_or("Revoking device not in group")?;
if !revoker_perms.contains(Permissions::MANAGE_ROLES) && !revoker_perms.contains(Permissions::ADMINISTRATOR) { return Err("Insufficient permissions to manage roles".into()); }
// Cannot revoke founder's permissions if target_device == perm_ext.founder { return Err("Cannot revoke founder's permissions".into()); }
// Update target's permissions if let Some(target_perms) = perm_ext.device_permissions.get_mut(&target_device) { *target_perms = target_perms.difference(permission_to_revoke); }
perm_ext.version += 1; perm_ext.last_updated_at = current_unix_timestamp(); perm_ext.last_updated_by = revoking_device.device_id;
// Same commit/merge/distribute process as granting... // (See grant_permission_to_member for full implementation)
Ok(())}Permission Rules:
- Only
MANAGE_ROLESorADMINISTRATORcan change permissions - Founder’s
ADMINISTRATORpermission is immutable - Permission changes are logged in group history
- All clients must verify permission changes before accepting
See Moderation Architecture for the complete permission flag list and presets.
Group Metadata Extension
Section titled “Group Metadata Extension”Group metadata (name, picture, description, and thread organization) is stored in an MLS group context extension, ensuring that all cosmetic and organizational data is cryptographically protected and automatically synchronized across members.
Extension Structure
Section titled “Extension Structure”const CRYPTID_GROUP_METADATA_EXT: u16 = 0xF001;
struct CryptidGroupMetadata { // Group cosmetics group_name: Option<String>, group_picture: Option<GroupPicture>, group_description: Option<String>,
// Thread organization threads: HashMap<ThreadId, ThreadMetadata>,
// Version tracking version: u64, last_updated_at: u64, last_updated_by: DeviceId,}
struct GroupPicture { // Small images stored inline (< 100KB) inline_data: Option<InlineMedia>, // Large images via file transfer reference file_reference: Option<FileReference>,}
struct ThreadMetadata { id: ThreadId, name: String, created_at: u64, created_by: DeviceId, pinned: bool, archived: bool,}
impl MultiUserGroup { /// Extract metadata from MLS group context fn get_group_metadata_extension(&self) -> Result<CryptidGroupMetadata> { let extensions = self.mls_state.group_context().extensions();
let metadata_ext = extensions .iter() .find(|ext| matches!( ext.extension_type(), ExtensionType::Unknown(CRYPTID_GROUP_METADATA_EXT) )) .ok_or("Group metadata extension missing")?;
let metadata: CryptidGroupMetadata = serde_json::from_slice(metadata_ext.extension_data())?;
Ok(metadata) }}Updating Group Name
Section titled “Updating Group Name”async fn update_group_name( group: &mut MultiUserGroup, new_name: String, updating_device: &DeviceIdentity, provider: &impl OpenMlsProvider,) -> Result<()> { // 1. Check permissions let perm_ext = group.get_permission_extension()?; let updater_perms = perm_ext.device_permissions .get(&updating_device.device_id) .ok_or("Updating device not in group")?;
if !updater_perms.contains(Permissions::CHANGE_GROUP_NAME) && !updater_perms.contains(Permissions::ADMINISTRATOR) { return Err("Insufficient permissions to change group name".into()); }
// 2. Update metadata let mut metadata = group.get_group_metadata_extension()?; metadata.group_name = Some(new_name); metadata.version += 1; metadata.last_updated_at = current_unix_timestamp(); metadata.last_updated_by = updating_device.device_id;
// 3. Create MLS commit let extension_data = serde_json::to_vec(&metadata)?; let extension = Extension::new( ExtensionType::Unknown(CRYPTID_GROUP_METADATA_EXT), extension_data.into(), );
let (commit, _) = group.mls_state.update_group_context_extensions( provider, Extensions::single(extension), &updating_device.signer, )?;
// 4. Merge and distribute group.mls_state.merge_pending_commit(provider)?;
let commit_message = commit.tls_serialize_detached()?; for member in &group.members { for device_id in &member.devices_in_group { let secure_message = SecureMessage { message_id: Uuid::now_v7(), group_id: group.group_id, mls_ciphertext: commit_message.clone(), sender_signature: updating_device.sign(&commit_message), timestamp: current_unix_timestamp(), message_type: MessageType::SystemOperation, };
if let Some(device_info) = find_device_info(device_id) { send_message(secure_message, &device_info.initial_delivery_address)?; } } }
Ok(())}Updating Group Picture
Section titled “Updating Group Picture”async fn update_group_picture( group: &mut MultiUserGroup, picture: GroupPicture, updating_device: &DeviceIdentity, provider: &impl OpenMlsProvider,) -> Result<()> { // Check permissions let perm_ext = group.get_permission_extension()?; let updater_perms = perm_ext.device_permissions .get(&updating_device.device_id) .ok_or("Updating device not in group")?;
if !updater_perms.contains(Permissions::CHANGE_GROUP_ICON) && !updater_perms.contains(Permissions::ADMINISTRATOR) { return Err("Insufficient permissions to change group picture".into()); }
// Update metadata (same pattern as update_group_name) let mut metadata = group.get_group_metadata_extension()?; metadata.group_picture = Some(picture); metadata.version += 1; metadata.last_updated_at = current_unix_timestamp(); metadata.last_updated_by = updating_device.device_id;
// Create commit and distribute (same stuff as update_group_name)}Managing Threads
Section titled “Managing Threads”Threads organize messages within a group. Each thread has a name, creation metadata, and status flags.
Creating a Thread
Section titled “Creating a Thread”async fn create_thread( group: &mut MultiUserGroup, thread_name: String, creating_device: &DeviceIdentity, provider: &impl OpenMlsProvider,) -> Result<ThreadId> { // Check permissions let perm_ext = group.get_permission_extension()?; let creator_perms = perm_ext.device_permissions .get(&creating_device.device_id) .ok_or("Creating device not in group")?;
if !creator_perms.contains(Permissions::MANAGE_THREADS) && !creator_perms.contains(Permissions::ADMINISTRATOR) { return Err("Insufficient permissions to create threads".into()); }
// Generate thread ID let thread_id = Uuid::new_v4();
// Update metadata let mut metadata = group.get_group_metadata_extension()?; metadata.threads.insert( thread_id, ThreadMetadata { id: thread_id, name: thread_name, created_at: current_unix_timestamp(), created_by: creating_device.device_id, pinned: false, archived: false, } ); metadata.version += 1; metadata.last_updated_at = current_unix_timestamp(); metadata.last_updated_by = creating_device.device_id;
// Create commit and distribute (same pattern as other metadata updates) let extension_data = serde_json::to_vec(&metadata)?; let extension = Extension::new( ExtensionType::Unknown(CRYPTID_GROUP_METADATA_EXT), extension_data.into(), );
let (commit, _) = group.mls_state.update_group_context_extensions( provider, Extensions::single(extension), &creating_device.signer, )?;
group.mls_state.merge_pending_commit(provider)?;
// Distribute commit to all members
Ok(thread_id)}Renaming a Thread
Section titled “Renaming a Thread”async fn rename_thread( group: &mut MultiUserGroup, thread_id: ThreadId, new_name: String, renaming_device: &DeviceIdentity, provider: &impl OpenMlsProvider,) -> Result<()> { // Check permissions let perm_ext = group.get_permission_extension()?; let renamer_perms = perm_ext.device_permissions .get(&renaming_device.device_id) .ok_or("Renaming device not in group")?;
if !renamer_perms.contains(Permissions::MANAGE_THREADS) && !renamer_perms.contains(Permissions::ADMINISTRATOR) { return Err("Insufficient permissions to rename threads".into()); }
// Update thread name let mut metadata = group.get_group_metadata_extension()?; let thread = metadata.threads.get_mut(&thread_id) .ok_or("Thread not found")?;
thread.name = new_name; metadata.version += 1; metadata.last_updated_at = current_unix_timestamp(); metadata.last_updated_by = renaming_device.device_id;
// Create commit and distribute}Pinning/Archiving Threads
Section titled “Pinning/Archiving Threads”async fn update_thread_status( group: &mut MultiUserGroup, thread_id: ThreadId, pinned: Option<bool>, archived: Option<bool>, updating_device: &DeviceIdentity, provider: &impl OpenMlsProvider,) -> Result<()> { // Permission check let perm_ext = group.get_permission_extension()?; let updater_perms = perm_ext.device_permissions .get(&updating_device.device_id) .ok_or("Updating device not in group")?;
if !updater_perms.contains(Permissions::MANAGE_THREADS) && !updater_perms.contains(Permissions::ADMINISTRATOR) { return Err("Insufficient permissions to update thread status".into()); }
let mut metadata = group.get_group_metadata_extension()?; let thread = metadata.threads.get_mut(&thread_id) .ok_or("Thread not found")?;
if let Some(pin) = pinned { thread.pinned = pin; } if let Some(arch) = archived { thread.archived = arch; }
metadata.version += 1; metadata.last_updated_at = current_unix_timestamp(); metadata.last_updated_by = updating_device.device_id;
// Create commit and distribute...}Address Rotation in Groups
Section titled “Address Rotation in Groups”When a group member rotates their delivery addresses, they notify the group via a system message.
Address Rotation Protocol
Section titled “Address Rotation Protocol”fn rotate_address_in_group( group: &mut MultiUserGroup, device: &DeviceIdentity, new_address: String, provider: &impl OpenMlsProvider,) -> Result<()> { // 1. Create system message announcing address change let address_rotation = SystemOperation::AddressRotation { device_id: device.device_id, new_delivery_address: new_address.clone(), timestamp: current_unix_timestamp(), };
// 2. Serialize and sign the message let message_bytes = serialize(&address_rotation)?; let signature = device.sign(&message_bytes);
// 3. Encrypt with MLS let encrypted = group.mls_state.encrypt_application_message( provider, &device.keypair, &message_bytes, )?;
// 4. Send to all group members for member in &group.members { let secure_message = SecureMessage { message_id: Uuid::now_v7(), group_id: group.group_id, mls_ciphertext: encrypted.clone(), sender_signature: signature.clone(), timestamp: current_unix_timestamp(), message_type: MessageType::SystemOperation, }; send_message( secure_message, &member.delivery_addresses, )?; }
// 5. Update local group state if let Some(member) = group.members.iter_mut() .find(|m| m.device_id == device.device_id) { member.delivery_addresses.push(new_address); }
Ok(())}Receiving Address Rotation
Section titled “Receiving Address Rotation”When members receive an address rotation message:
fn handle_address_rotation( group: &mut MultiUserGroup, rotation: &SystemOperation::AddressRotation,) -> Result<()> { // 1. Find the member let member = group.members.iter_mut() .find(|m| m.device_id == rotation.device_id) .ok_or("Unknown member")?;
// 2. Add new address to their list if !member.delivery_addresses.contains(&rotation.new_delivery_address) { member.delivery_addresses.push(rotation.new_delivery_address.clone()); }
// 3. Optional: Remove old addresses after grace period // (Keep old addresses for a few days to handle in-flight messages)
Ok(())}Address rotation properties:
- Members maintain multiple addresses during transition
- Old addresses remain valid for a grace period (e.g., 7 days)
- Group members automatically learn new addresses
- No manual contact updates needed
Federation Protocol
Section titled “Federation Protocol”Server Discovery
Section titled “Server Discovery”GET /.well-known/cryptidHost: server.example.com{ "protocol_version": "1.0", "server_name": "server.example.com", "federation_enabled": true, "supported_features": [ "mls_messaging" ], "rate_limits": { "federated_messages_per_minute": 1000, "max_message_size": 10485760 }, "certificate_fingerprint": "sha256:a1b2c3d4..."}Cross-Server Message Delivery
Section titled “Cross-Server Message Delivery”PUT /federation/v1/deliver/{transaction_id}Content-Type: application/jsonAuthorization: Bearer <federation_token>{ "origin_server": "alice.server1.com", "transaction_id": "23bcb253-6fb0-4369-9545-706b0a863da3", "messages": [ { "message_id": "019a821b-d8d4-7dc1-8ea4-28dfcf55346b", "recipient_addresses": ["f9c66cde-be8c-43c7-853a-fb1307755cc2@server2.com"], "group_id": "b79da4c3-a5aa-451d-90be-45336b0522ee", "mls_ciphertext": "base64_of_encrypted_content", "sender_signature": "ed25519_signature", "timestamp": 1758999498 } ]}Recipient Address Handling
Section titled “Recipient Address Handling”The recipient_addresses field contains delivery addresses (not device_ids). If a device has multiple delivery addresses on different servers, the message may be sent to any active address. The receiving server routes to the device regardless of which address was used.
Example:
{ "origin_server": "server1.com", "messages": [ { "recipient_addresses": [ "bob-work@server2.com", // Bob's work address "bob-personal@server2.com" // Bob's personal address (same device) ], // Server2 delivers to Bob's device via either address } ]}