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
| Artifact | Path |
|---|---|
| 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.style | Chat Type |
|---|---|
| 43 | Group conversation |
| 45 | Individual (one-to-one) conversation |
Chat GUID Format
The chat.guid column encodes the chat type in its format:
| Pattern | Example | Meaning |
|---|---|---|
iMessage;+;chatNNNNNN | iMessage;+;chat123456789 | Group iMessage chat (note the +) |
iMessage;-;+15551234567 | iMessage;-;+15551234567 | Individual iMessage chat (note the -) |
SMS;-;+15551234567 | SMS;-;+15551234567 | Individual 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
| Column | Description |
|---|---|
chat.guid | Unique chat identifier (includes + for groups) |
chat.style | 43 for group chats |
chat.display_name | User-assigned group name (may be NULL) |
chat.chat_identifier | Group identifier string |
chat.is_archived | Whether the chat has been archived |
chat.ck_sync_state | CloudKit sync status |
chat.last_read_message_timestamp | Last 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 likechat123456789.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_joinfor a givenchat_idto 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 = 1andgroup_action_typeset) that records the name change. Thegroup_titlecolumn 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_typevalues onmessagerows associated with the group chat. - The local user is implicit: The
chat_handle_jointable 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_ididentifies which participant sent it. Messages sent by the local user haveis_from_me = 1andhandle_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 Version | Changes |
|---|---|
| 10.15 Catalina | Baseline group chat support |
| 11 Big Sur | Inline replies within groups (thread_originator_guid) |
| 13 Ventura | Mentions in group chats (has_unseen_mention) |
| 14 Sonoma | Enhanced group management |
Tool Support
| Tool | Capability |
|---|---|
| macfor | Dedicated group chat metadata collection with participant enumeration |
| sqlite3 CLI | Manual querying of chat and chat_handle_join tables |