WhatsApp Desktop

WhatsApp Desktop — Chat Database

Overview

ChatStorage.sqlite is the primary forensic artifact for WhatsApp Desktop on macOS. It is a standard, unencrypted SQLite database using Apple's Core Data persistence framework. All entity table names carry the ZWA prefix (e.g., ZWAMESSAGE, ZWACHATSESSION). The database is located at:

~/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite

The database is accessible to any process running as the user — no Full Disk Access, no root privileges, and no decryption keys are required.

Always collect the WAL and SHM companion files alongside the main database:

ChatStorage.sqlite
ChatStorage.sqlite-wal
ChatStorage.sqlite-shm

Timestamp Format

All timestamp columns use the Apple Core Data epoch: floating-point seconds since January 1, 2001, 00:00:00 UTC.

unix_timestamp = core_data_timestamp + 978307200

In SQLite queries:

datetime(ZMESSAGEDATE + 978307200, 'unixepoch') AS message_time

Zero values in timestamp columns indicate the timestamp was not populated. The ZPIN column on ZWACHATSESSION stores a non-zero value when the session is pinned (the value itself is a timestamp of when pinning occurred, not a boolean).

Table: ZWAMESSAGE

The central table. Every message in every conversation is stored here — text messages, media messages, system events, and missed call notifications.

ColumnTypeDescription
Z_PKINTEGERCore Data primary key
Z_ENTINTEGERCore Data entity type ID
Z_OPTINTEGERCore Data optimistic locking counter
ZCHATSESSIONINTEGERFK → ZWACHATSESSION.Z_PK
ZMEDIAITEMINTEGERFK → ZWAMEDIAITEM.Z_PK (null for text messages)
ZFROMJIDVARCHARSender JID — <phone>@s.whatsapp.net or <id>@g.us
ZTOJIDVARCHARRecipient JID (null for incoming messages)
ZSTANZAIDVARCHARUnique message ID from the XMPP stanza
ZPUSHNAMEVARCHARSender's display name at time of sending
ZTEXTVARCHARPlaintext message body
ZMESSAGETYPEINTEGERMessage type constant (see below)
ZISFROMMEINTEGER1 = outgoing (sent by account owner), 0 = incoming
ZMESSAGESTATUSINTEGERDelivery/read status (see below)
ZMESSAGEDATETIMESTAMPWhen the message was created or received (Core Data epoch)
ZSENTDATETIMESTAMPWhen the message was sent — populated for outgoing messages
ZSERVERCREATIONDATETIMESTAMPServer-assigned timestamp
ZSTARREDINTEGER1 = user starred this message
ZISFLAGINTEGERInternal flag status
ZISFORWARDEDINTEGER1 = message was forwarded from another conversation
ZFORWARDCOUNTINTEGERNumber of times the message has been forwarded
ZQUOTEDMESSAGEINTEGERFK → ZWAMESSAGE.Z_PK of the message being replied to
ZGROUPEVENTTYPEINTEGERGroup event subtype for system messages
ZGROUPMEMBERINTEGERFK → ZWAGROUPMEMBER.Z_PK for group events
ZMEDIASECTIONIDVARCHARMedia section identifier
ZMESSAGEACTIONTYPEINTEGERAction type for system-generated messages
ZSORTINTEGERSort order within the conversation
ZFLAGSINTEGERBitmask of message feature flags
ZDATAITEMVERSIONINTEGERExtended data schema version
ZPARENTMESSAGEINTEGERFK for threaded replies
ZCHILDMESSAGESDELIVEREDCOUNTINTEGEROutgoing group delivery count
ZCHILDMESSAGESPLAYEDCOUNTINTEGEROutgoing group played count (voice/video)
ZCHILDMESSAGESREADCOUNTINTEGEROutgoing group read count

Schema Evolution

The ZWAMESSAGE schema has expanded significantly across WhatsApp releases. Newer versions (2024–2025) may include additional columns for message editing, view-once media, disappearing message state, and WhatsApp Channels. The macfor collector uses dynamic column detection to handle schema variations gracefully.

ZMESSAGETYPE Values

ValueNameDescription
0textPlain text message
1imagePhoto or image attachment
2videoVideo attachment
3voiceVoice message or audio note
4contactvCard / contact card share
5locationGPS location or live location pin
6systemGroup event (member added/removed, name change, subject update)
7linkURL with web preview
8documentFile attachment (PDF, DOCX, etc.)
10missed_callMissed voice or video call notification
13gifAnimated GIF
15stickerSticker message

Values 9, 11, 12, and 14 have been observed but are not officially documented. The macfor collector returns unknown(N) for unrecognized values.

ZMESSAGESTATUS Values

ValueNameDescription
0unknownMessage not yet sent, or locally received with no status set
1receivedMessage received by the server (single checkmark)
2deliveredDelivered to recipient device (double checkmark, gray)
3readRead by recipient (double checkmark, blue) or voice message played
4server_ackServer acknowledgment
5deliveredAlternative delivery state
6readAlternative read/played state
8read_confirmedConfirmed read receipt

For incoming messages, ZMESSAGESTATUS typically reflects how the message was handled locally. For outgoing group messages, it reflects aggregate delivery state — ZWAMESSAGEINFO provides per-recipient detail.

Table: ZWACHATSESSION

One row per conversation thread. Covers 1:1 conversations, group chats, broadcast lists, and the status feed.

ColumnTypeDescription
Z_PKINTEGERCore Data primary key
Z_ENTINTEGERCore Data entity type ID
Z_OPTINTEGERCore Data optimistic locking counter
ZCONTACTJIDVARCHARConversation JID — <phone>@s.whatsapp.net or <id>@g.us
ZPARTNERNAMEVARCHARDisplay name of the contact or group
ZSESSIONTYPEINTEGERSession type constant (see below)
ZMESSAGECOUNTERINTEGERTotal messages in this conversation (historical count)
ZUNREADCOUNTINTEGERUnread message count at time of collection
ZLASTMESSAGEINTEGERFK → ZWAMESSAGE.Z_PK of the most recent message
ZLASTMESSAGEDATETIMESTAMPCore Data timestamp of the last message
ZLASTMESSAGETEXTVARCHARPreview text of the last message
ZARCHIVEDINTEGER1 = conversation has been archived by the user
ZMUTEEXPIRATIONTIMESTAMPMute-until timestamp (Core Data epoch), null if not muted
ZPINTIMESTAMPNon-zero = pinned conversation (value is the pin timestamp)
ZGROUPINFOINTEGERFK → ZWAGROUPINFO.Z_PK (null for 1:1 conversations)
ZCREATIONDATETIMESTAMPWhen this session was first created (Core Data epoch)
ZDISAPPEARINGMESSAGEDURATIONINTEGERAuto-delete timer in seconds, 0 = disabled
ZLOCATIONSHARINGCONTACTINTEGERFK for live location sharing

ZSESSIONTYPE Values

ValueNameDescription
0private1:1 conversation
1groupGroup chat
2broadcastBroadcast list
3statusStatus updates feed

Table: ZWAMEDIAITEM

Stores metadata for all media attachments. One row per media item, linked to its parent message via ZMESSAGE.

ColumnTypeDescription
Z_PKINTEGERCore Data primary key
Z_ENTINTEGERCore Data entity type ID
Z_OPTINTEGERCore Data optimistic locking counter
ZMESSAGEINTEGERFK → ZWAMESSAGE.Z_PK
ZMEDIALOCALPATHVARCHARRelative path to the downloaded media file in the Group Container
ZMEDIAURLVARCHARRemote URL on the WhatsApp CDN for download or re-download
ZVCARDSTRINGVARCHARvCard data for contact shares
ZVCARDNAMEVARCHARContact name or media file identifier
ZMEDIAKEYBLOBMedia decryption key (for CDN re-download)
ZTITLEVARCHARMedia title or original filename
ZFILESIZEINTEGERFile size in bytes
ZFILEMIMETYPEVARCHARMIME type (e.g., image/jpeg, video/mp4, application/pdf)
ZWIDTHINTEGERImage or video width in pixels
ZHEIGHTINTEGERImage or video height in pixels
ZASPECTRATIOFLOATAspect ratio
ZMOVIEDURATIONINTEGERDuration in seconds for audio and video
ZLATITUDEFLOATLatitude for location messages
ZLONGITUDEFLOATLongitude for location messages
ZTHUMBNAILLOCALPATHVARCHARPath to the local thumbnail image
ZTHUMBNAILDATABLOBEmbedded thumbnail binary data
ZDOWNLOADRETRYCOUNTINTEGERNumber of download retry attempts
ZMEDIAUPLOADEDFROMTARGETINTEGERInternal upload tracking flag

ZMEDIALOCALPATH values are relative to the Group Container directory. To construct the full path, prepend ~/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/.

Location messages (type 5) populate ZLATITUDE and ZLONGITUDE instead of file-related fields.

Table: ZWAGROUPINFO

Group-level metadata. One row per group chat, linked from ZWACHATSESSION.ZGROUPINFO.

ColumnTypeDescription
Z_PKINTEGERCore Data primary key
Z_ENTINTEGERCore Data entity type ID
Z_OPTINTEGERCore Data optimistic locking counter
ZCHATSESSIONINTEGERFK → ZWACHATSESSION.Z_PK
ZCREATORJIDVARCHARJID of the user who created the group
ZSUBJECTVARCHARCurrent group name (subject)
ZDESCRIPTIONVARCHARGroup description text
ZSUBJECTOWNERVARCHARJID of the user who last set the subject
ZCREATIONDATETIMESTAMPGroup creation timestamp (Core Data epoch)
ZSUBJECTDATETIMESTAMPWhen the subject was last changed (Core Data epoch)

Table: ZWAGROUPMEMBER

Group membership. One row per member per group. Group chats are identified by ZCHATSESSION.

ColumnTypeDescription
Z_PKINTEGERCore Data primary key
Z_ENTINTEGERCore Data entity type ID
Z_OPTINTEGERCore Data optimistic locking counter
ZCHATSESSIONINTEGERFK → ZWACHATSESSION.Z_PK
ZMEMBERJIDVARCHARMember's JID — <phone>@s.whatsapp.net
ZISADMININTEGERAdmin role: 0 = member, 1 = admin, 2 = superadmin
ZCONTACTNAMEVARCHARMember's display name at time of last observation

The ZISADMIN value 2 (superadmin) typically indicates the original group creator.

Table: ZWAMESSAGEINFO

Per-recipient delivery and read receipts for outgoing messages. Only populated for messages sent by the account owner. In group conversations, there is one row per recipient.

ColumnTypeDescription
Z_PKINTEGERCore Data primary key
Z_ENTINTEGERCore Data entity type ID
Z_OPTINTEGERCore Data optimistic locking counter
ZMESSAGEINTEGERFK → ZWAMESSAGE.Z_PK
ZRECEIPTJIDVARCHARRecipient JID who triggered this receipt
ZREADDATETIMESTAMPWhen the recipient read the message (Core Data epoch)
ZRECEIVEDDATETIMESTAMPWhen the recipient's device received the message (Core Data epoch)
ZPLAYEDDATETIMESTAMPWhen the recipient played audio or video (Core Data epoch)

A non-null ZREADDATE is strong evidence that the recipient read the specific message on their device.

Table: ZWAPROFILEPUSHNAME

Maps JIDs to the display name the contact has set for themselves (their "push name"). This is the name the contact chose in their WhatsApp profile, distinct from the name stored in the local address book.

ColumnTypeDescription
Z_PKINTEGERCore Data primary key
Z_ENTINTEGERCore Data entity type ID
Z_OPTINTEGERCore Data optimistic locking counter
ZJIDVARCHARContact JID — <phone>@s.whatsapp.net
ZPUSHNAMEVARCHARThe contact's self-set display name

This table is useful when the contact is not in the local address book — the push name provides an identity label from the contact themselves.

Table: Z_PRIMARYKEY

Core Data entity registry. The Z_MAX column stores the highest primary key value ever assigned for each entity type, providing a historical record count even after deletions.

ColumnTypeDescription
Z_ENTINTEGEREntity type ID (matches Z_ENT in all other tables)
Z_NAMEVARCHAREntity name (e.g., WAMessage, WAChatSession)
Z_SUPERINTEGERParent entity ID (for inheritance)
Z_MAXINTEGERHighest Z_PK ever assigned — a lower bound on historical record count

Query example: SELECT Z_NAME, Z_MAX FROM Z_PRIMARYKEY ORDER BY Z_MAX DESC shows all entity types and their maximum historical counts.

Key SQL Queries

All messages in a conversation

-- List all messages in a specific conversation, newest first.
-- Replace the JID placeholder with the target phone number.
SELECT
    m.Z_PK          AS message_id,
    datetime(m.ZMESSAGEDATE + 978307200, 'unixepoch') AS sent_at,
    m.ZFROMJID      AS sender_jid,
    m.ZISFROMME     AS is_from_me,
    m.ZMESSAGETYPE  AS msg_type,
    m.ZMESSAGESTATUS AS status,
    m.ZTEXT         AS body,
    m.ZSTANZAID     AS stanza_id
FROM ZWAMESSAGE m
JOIN ZWACHATSESSION cs ON m.ZCHATSESSION = cs.Z_PK
WHERE cs.ZCONTACTJID = '14155551234@s.whatsapp.net'
ORDER BY m.ZMESSAGEDATE DESC;

All conversations with last message date

-- List all conversation threads, ordered by most recent activity.
SELECT
    cs.ZCONTACTJID                                             AS chat_jid,
    cs.ZPARTNERNAME                                            AS name,
    CASE cs.ZSESSIONTYPE
        WHEN 0 THEN 'private'
        WHEN 1 THEN 'group'
        WHEN 2 THEN 'broadcast'
        WHEN 3 THEN 'status'
        ELSE 'unknown'
    END                                                        AS session_type,
    cs.ZMESSAGECOUNTER                                         AS total_messages,
    cs.ZUNREADCOUNT                                            AS unread,
    datetime(cs.ZLASTMESSAGEDATE + 978307200, 'unixepoch')    AS last_message_at,
    cs.ZARCHIVED                                               AS is_archived
FROM ZWACHATSESSION cs
ORDER BY cs.ZLASTMESSAGEDATE DESC;

Outgoing messages with full delivery/read confirmation

-- Find outgoing messages where a specific recipient confirmed they read the message.
SELECT
    m.Z_PK AS message_id,
    datetime(m.ZMESSAGEDATE + 978307200, 'unixepoch') AS sent_at,
    cs.ZCONTACTJID AS chat_jid,
    m.ZTEXT AS body,
    mi.ZRECEIPTJID AS read_by,
    datetime(mi.ZREADDATE + 978307200, 'unixepoch') AS read_at,
    datetime(mi.ZRECEIVEDDATE + 978307200, 'unixepoch') AS received_at
FROM ZWAMESSAGE m
JOIN ZWACHATSESSION cs ON m.ZCHATSESSION = cs.Z_PK
JOIN ZWAMESSAGEINFO mi ON mi.ZMESSAGE = m.Z_PK
WHERE m.ZISFROMME = 1
  AND mi.ZREADDATE IS NOT NULL
  AND mi.ZREADDATE > 0
ORDER BY m.ZMESSAGEDATE DESC;

Messages with media attachments (geolocation)

-- Find all location messages with latitude/longitude coordinates.
SELECT
    m.Z_PK AS message_id,
    datetime(m.ZMESSAGEDATE + 978307200, 'unixepoch') AS sent_at,
    cs.ZCONTACTJID AS chat_jid,
    m.ZFROMJID AS sender_jid,
    med.ZLATITUDE AS latitude,
    med.ZLONGITUDE AS longitude,
    m.ZTEXT AS location_label
FROM ZWAMESSAGE m
JOIN ZWACHATSESSION cs ON m.ZCHATSESSION = cs.Z_PK
JOIN ZWAMEDIAITEM med ON med.ZMESSAGE = m.Z_PK
WHERE m.ZMESSAGETYPE = 5
  AND med.ZLATITUDE IS NOT NULL
ORDER BY m.ZMESSAGEDATE DESC;

Forwarded messages

-- Find all forwarded messages, which may indicate information dissemination.
SELECT
    m.Z_PK AS message_id,
    datetime(m.ZMESSAGEDATE + 978307200, 'unixepoch') AS sent_at,
    cs.ZCONTACTJID AS chat_jid,
    m.ZFROMJID AS sender_jid,
    m.ZISFROMME AS is_from_me,
    m.ZFORWARDCOUNT AS forward_count,
    m.ZTEXT AS body
FROM ZWAMESSAGE m
JOIN ZWACHATSESSION cs ON m.ZCHATSESSION = cs.Z_PK
WHERE m.ZISFORWARDED = 1
ORDER BY m.ZMESSAGEDATE DESC;

Group membership and admin roles

-- List all members of a group chat with their admin roles.
SELECT
    cs.ZPARTNERNAME AS group_name,
    cs.ZCONTACTJID AS group_jid,
    gi.ZCREATORJID AS creator_jid,
    datetime(gi.ZCREATIONDATE + 978307200, 'unixepoch') AS group_created_at,
    gm.ZMEMBERJID AS member_jid,
    CASE gm.ZISADMIN
        WHEN 0 THEN 'member'
        WHEN 1 THEN 'admin'
        WHEN 2 THEN 'superadmin'
        ELSE 'unknown'
    END AS role,
    gm.ZCONTACTNAME AS display_name
FROM ZWAGROUPMEMBER gm
JOIN ZWACHATSESSION cs ON gm.ZCHATSESSION = cs.Z_PK
LEFT JOIN ZWAGROUPINFO gi ON cs.ZGROUPINFO = gi.Z_PK
WHERE cs.ZCONTACTJID = '120363123456789012@g.us'
ORDER BY gm.ZISADMIN DESC, gm.ZMEMBERJID;

Starred messages

-- Retrieve all starred (bookmarked) messages across all conversations.
SELECT
    m.Z_PK AS message_id,
    datetime(m.ZMESSAGEDATE + 978307200, 'unixepoch') AS sent_at,
    cs.ZCONTACTJID AS chat_jid,
    cs.ZPARTNERNAME AS chat_name,
    m.ZFROMJID AS sender_jid,
    m.ZMESSAGETYPE AS msg_type,
    m.ZTEXT AS body
FROM ZWAMESSAGE m
JOIN ZWACHATSESSION cs ON m.ZCHATSESSION = cs.Z_PK
WHERE m.ZSTARRED = 1
ORDER BY m.ZMESSAGEDATE DESC;

Historical record count from Core Data

-- Show entity types and their maximum historical primary key values.
-- Z_MAX is a lower bound on total records ever created.
SELECT
    Z_NAME AS entity,
    Z_MAX AS max_pk_ever_assigned
FROM Z_PRIMARYKEY
ORDER BY Z_MAX DESC;

Media files by MIME type

-- List all media attachments by MIME type with local file paths.
SELECT
    med.ZFILEMIMETYPE AS mime_type,
    count(*) AS count,
    sum(med.ZFILESIZE) AS total_bytes
FROM ZWAMEDIAITEM med
WHERE med.ZFILEMIMETYPE IS NOT NULL
GROUP BY med.ZFILEMIMETYPE
ORDER BY count DESC;

macfor Collection

macfor collects the following artifacts from ChatStorage.sqlite via the messaging.whatsapp plugin:

Record TypeSource TableDescription
whatsapp_messagesZWAMESSAGE + ZWACHATSESSION + ZWAMEDIAITEMAll messages with media joins
whatsapp_conversationsZWACHATSESSIONConversation thread metadata

The raw ChatStorage.sqlite, ChatStorage.sqlite-wal, and ChatStorage.sqlite-shm files are also preserved in the evidence container.

Collection command:

macfor-pro collect --plugin messaging.whatsapp --output ./evidence.zip

Skip message bodies (metadata-only mode):

macfor-pro collect --plugin messaging.whatsapp --option metadata_only=true --output ./evidence.zip

References

Previous
WhatsApp Overview