Skip to content

ContactRequest

ContactRequest is the message type used for initial contact establishment between devices that are not yet in any shared MLS group. It handles the bootstrapping of new contacts and direct message groups.

When two devices want to establish their first contact (e.g., via a QR code scan), they need to:

  1. Create a new MLS group for 1:1 chat
  2. Exchange identity information
  3. Store each other as contacts

ContactRequest handles this entire flow in a single message type.

ContactRequest Structure
/// Contact request identifier (UUIDv7)
pub struct RequestId(Uuidv7);
/// Initial ContactRequest
struct ContactRequest {
// UUIDv7 for ordering
request_id: RequestId,
// Device sending the request
sender: DeviceId,
// Their UserIdentity (for contact store)
user_identity: UserIdentity,
// Their devices (each contains delivery address and keypackage server)
// Recipient fetches KeyPackages from each device's keypackage_server
devices: Vec<DevicePublicInfo>,
}
  • request_id
    • Wrapper around Uuidv7
    • Unique identifier for this contact request
    • Used for deduplication and ordering
  • sender
    • DeviceId of the device sending the request
    • Authenticated by the sender’s device key
  • user_identity
    • UserIdentity of the requester
    • Stored in the recipient’s contact store
    • See UserIdentity
  • devices
    • All devices controlled by this user
    • Used to create the DirectMessageGroup with all their devices
    • See DevicePublicInfo
sequenceDiagram
Alice ->> Relay Server: Upload KeyPackages to Server
Alice ->> Bob: Scan QR / Exchange InfoPackage
Alice ->> Bob: Create DirectMessageGroup and Send ContactRequest
Bob ->> Relay Server: Fetch KeyPackage from server for each device in devices
Bob ->> Alice: Receive MLSWelcome
Alice --> Bob: Both in DM group, ready to chat
  1. KeyPackage Upload

    Alice’s devices upload KeyPackages to their respective keypackage_server:

    • Each device uploads 50-100 KeyPackages
    • KeyPackages are stored by device_id on the server
    • The server URL for each device is stored in DevicePublicInfo
    • See KeyPackages
  2. InfoPackage Exchange

    Both devices exchange InfoPackages (via QR code, NFC, or link):

    • Contains: UserIdentity, DevicePublicInfo array
    • Does NOT contain KeyPackages (they’re on the respective servers)
    • See InfoPackages
  3. Create DirectMessageGroup

    The requester creates a new MLS group:

    • Group ID is deterministic: Blake3(sorted(user1_id, user2_id))
    • Both users are founders
  4. Send ContactRequest

    Requester sends ContactRequest with:

    • Their UserIdentity (for contact store)
    • Their DevicePublicInfo array
  5. Fetch KeyPackages

    Recipient receives ContactRequest and:

    1. Extracts device IDs from the devices field
    2. For each device, fetch a KeyPackage from that device’s keypackage_server
    3. Validates KeyPackage signatures and credentials
  6. Add Members and Send Welcome

    Recipient:

    • Creates the same DirectMessageGroup (same group ID)
    • Stores the sender’s UserIdentity in contact store
    • Adds sender’s devices to the group using the fetched KeyPackages
    • Sends MLSWelcome to the new devices
  7. Group Ready!

    Both devices now have:

    • A shared DirectMessageGroup
    • Each other in their contact store
    • Full encryption for 1:1 chat
  • Identity Verification:

    • Clients SHOULD verify the UserIdentity matches what was received via InfoPackage
    • Device signatures MUST be validated
    • Device IDs in ContactRequest MUST match UserIdentity’s device list
  • KeyPackage Validation:

    • Fetched KeyPackages MUST be valid (not expired, not used)
    • KeyPackage credential MUST match device_id
    • Clients MUST remove used KeyPackages from storage
  • Contact Store:
    • Contact information is stored locally only
    • Not shared with servers
    • Users can delete contacts at any time
  • How to exchange InfoPackages (QR, NFC, link, etc.)
  • When to show contact requests to users
  • How to display contacts in UI
  • Contact list management
  • Blocking/muting new contacts
{
"request_id": "019e607a-9846-7483-8698-83fbc5b8130a",
"sender": "device-abc123",
"user_identity": {
"user_id": "a1b2c3d4e5f6...",
"created_at": 1779735133,
"default_persona": {
"display_name": "Alice",
"profile_picture": null,
"bio": "Hello!",
"pronouns": null
},
"personas": {}
},
"devices": [
{
"device_id": "device-abc123",
"public_key": "base64encoded...",
"initial_delivery_address": {
"prefix": "alice-device-abc123",
"server": "chat.example.com"
},
"keypackage_server": "chat.example.com",
"linked_at": 1779734100
},
{
"device_id": "device-def456",
"public_key": "base64encoded...",
"initial_delivery_address": {
"prefix": "alice-device-def456",
"server": "chat.example.com"
},
"keypackage_server": "chat.example.com",
"linked_at": 1779734100
}
]
}

Group ID is deterministic to ensure both parties create the same group:

fn create_dm_group_id(user1: &UserId, user2: &UserId) -> GroupId {
let mut sorted = [*user1, *user2];
sorted.sort(); // Ensures same ID regardless of who creates group
blake3(&sorted.concat())
}