Messages

Group Chats

Overview

Group chats in Messages.app allow multi-party conversations over iMessage. Each group chat is represented as a row in the chat table with style = 43, and its participants are tracked through the chat_handle_join table. Group chats are forensically valuable because they reveal multi-party communication patterns, group membership changes, and organizational relationships.

Individual (one-to-one) conversations use chat.style = 45 and are not covered in this article.

File Locations

ArtifactPath
Group chat metadata~/Library/Messages/chat.db (chat table)
Participant mappings~/Library/Messages/chat.db (chat_handle_join table)

Database Schema

Identifying Group Chats

Group chats are distinguished by the style column in the chat table:

chat.styleChat Type
43Group conversation
45Individual (one-to-one) conversation

Chat GUID Format

The chat.guid column encodes the chat type in its format:

PatternExampleMeaning
iMessage;+;chatNNNNNNiMessage;+;chat123456789Group iMessage chat (note the +)
iMessage;-;+15551234567iMessage;-;+15551234567Individual iMessage chat (note the -)
SMS;-;+15551234567SMS;-;+15551234567Individual SMS chat

The + after the service name indicates a group chat; - indicates an individual chat.

Participant Resolution

Participants are linked through the chat_handle_join table:

CREATE TABLE chat_handle_join (
    chat_id INTEGER REFERENCES chat(ROWID) ON DELETE CASCADE,
    handle_id INTEGER REFERENCES handle(ROWID) ON DELETE CASCADE,
    UNIQUE(chat_id, handle_id)
);

To resolve participants to their identifiers:

SELECT
    c.ROWID AS chat_id,
    c.guid AS chat_guid,
    c.display_name AS group_name,
    h.id AS participant,
    h.service
FROM chat c
JOIN chat_handle_join chj ON c.ROWID = chj.chat_id
JOIN handle h ON chj.handle_id = h.ROWID
WHERE c.style = 43
ORDER BY c.ROWID, h.id;

Key Columns

ColumnDescription
chat.guidUnique chat identifier (includes + for groups)
chat.style43 for group chats
chat.display_nameUser-assigned group name (may be NULL)
chat.chat_identifierGroup identifier string
chat.is_archivedWhether the chat has been archived
chat.ck_sync_stateCloudKit sync status
chat.last_read_message_timestampLast read position

Key Fields for Analysis

  • display_name: The user-assigned name for the group (e.g., "Project Team", "Family"). This is NULL if the user never named the group, in which case the Messages UI shows a comma-separated list of participant names.
  • chat_identifier: The internal group identifier, typically a numeric string like chat123456789.
  • is_archived: Archived chats are hidden from the main conversation list but retain all data. A value of 1 indicates deliberate archival.
  • Participant count: Count the rows in chat_handle_join for a given chat_id to determine the number of participants. Note that the local user is typically not listed as a participant in this table.

Forensic Queries

-- List all group chats with participant counts
SELECT
    c.ROWID,
    c.guid,
    c.display_name,
    c.chat_identifier,
    c.is_archived,
    COUNT(chj.handle_id) AS participant_count
FROM chat c
JOIN chat_handle_join chj ON c.ROWID = chj.chat_id
WHERE c.style = 43
GROUP BY c.ROWID
ORDER BY participant_count DESC;

-- Group chat activity summary
SELECT
    c.display_name,
    c.chat_identifier,
    COUNT(cmj.message_id) AS message_count,
    datetime(MIN(m.date) / 1000000000 + 978307200, 'unixepoch') AS first_message,
    datetime(MAX(m.date) / 1000000000 + 978307200, 'unixepoch') AS last_message
FROM chat c
JOIN chat_message_join cmj ON c.ROWID = cmj.chat_id
JOIN message m ON cmj.message_id = m.ROWID
WHERE c.style = 43
GROUP BY c.ROWID
ORDER BY last_message DESC;

Timestamps

Group chat metadata does not have its own creation timestamp. The earliest message in chat_message_join for a group serves as a proxy for when the group was created. The last_read_message_timestamp column uses Core Data nanoseconds.

Analysis Notes

  • Group name changes: When a group name is changed, Messages.app creates a system message (with is_system_message = 1 and group_action_type set) that records the name change. The group_title column on these system messages contains the new group name.
  • Participant additions and removals: Adding or removing participants also generates system messages. These can be identified by group_action_type values on message rows associated with the group chat.
  • The local user is implicit: The chat_handle_join table typically lists only remote participants. The local user is implicitly a member of every chat in their database.
  • Cross-referencing: Participant handles (phone numbers and emails) can be resolved against the Contacts AddressBook database to identify participants by name. See Contacts - AddressBook.
  • Message attribution in groups: In a group chat, each message's handle_id identifies which participant sent it. Messages sent by the local user have is_from_me = 1 and handle_id = 0.
  • SMS group limitations: SMS-based group chats have more limited metadata compared to iMessage groups. Participant tracking may be less reliable.

Version Differences

macOS VersionChanges
10.15 CatalinaBaseline group chat support
11 Big SurInline replies within groups (thread_originator_guid)
13 VenturaMentions in group chats (has_unseen_mention)
14 SonomaEnhanced group management

Tool Support

ToolCapability
macforDedicated group chat metadata collection with participant enumeration
sqlite3 CLIManual querying of chat and chat_handle_join tables

References

Previous
Reactions