The attorney leans across the deposition table and asks: “How do you know this message was sent to my client specifically and not to a group?”
If you can’t answer that from the database schema, you shouldn’t be in that room.
iMessage evidence has put people in prison, ended careers, and decided civil disputes worth millions. The underlying database — sms.db — is well-documented enough that both sides in a dispute can verify your work. That’s a feature, not a bug. But it means your methodology needs to be airtight, and your court presentation needs to be grounded in how the database actually works, not how you assumed it works.
Where sms.db Lives and How to Get It
On iOS devices, the iMessage and SMS database is located at:
“`
/private/var/mobile/Library/SMS/sms.db
This path requires full file system access. You won't get sms.db itself in a standard logical extraction — you'll get a processed version of the messages through the backup protocol, but not the raw database file with its SQLite free pages, WAL file, or complete table structure.
For a true database-level examination, you need either a full file system extraction (see iPhone logical vs. full file system acquisition) or an encrypted iTunes backup, which includes a processed version of the SMS data (though not the raw SQLite file). For the raw database with deleted record recovery potential, FFS extraction is the path.
An additional file of interest:
``
/private/var/mobile/Library/SMS/sms.db-wal
The WAL (Write-Ahead Log) file for sms.db. This can contain recent transaction data including records added or modified since the last WAL checkpoint. Always capture both sms.db and sms.db-wal together.

Core Tables You Need to Understand
The sms.db schema has been relatively stable across recent iOS versions, though Apple occasionally adds columns. The four tables that matter most for court presentation:
The message Table
This is the central evidence table. Key columns:
- ROWID
: Primary key. Auto-incrementing integer. Higher ROWID = more recent message (generally, though not always — use the date column for actual chronology). - text
: The message content. NULL for some attachment-only messages. Edited messages may show the current text; prior versions are not stored in this table. - date
: Core Data timestamp — seconds since January 1, 2001. More on this below. - date_read
: Timestamp when the message was read (for received messages). - date_delivered
: Timestamp when delivery was confirmed. - is_from_me
: 0 = received, 1 = sent. - handle_id
: Foreign key to the handletable. Identifies who sent or received the message. - cache_roomnames
: For group messages, contains the room name identifier. NULL for individual messages. - service
: "iMessage" or "SMS". This matters — SMS metadata is different from iMessage. - guid
: A globally unique identifier for the message, generated client-side. - associated_message_guid
: For Tapback reactions (likes, hearts, thumbs up), this references the original message being reacted to. - reply_to_guid
: For threaded replies, this references the message being replied to. - was_downgraded
: Whether a message that was attempted as iMessage fell back to SMS.
The handle Table
This resolves message senders/recipients to actual phone numbers or Apple IDs.
- ROWID
: The handle_id referenced in the message table. - id
: Phone number (e.g., "+16195551234") or email address used as Apple ID. - service
: "iMessage" or "SMS". - uncanonicalized_id
: The non-normalized form of the identifier as it appeared.
A simple join to identify who sent each message:
`sql`
SELECT
m.ROWID,
datetime(m.date + 978307200, 'unixepoch', 'localtime') AS message_date,
h.id AS sender_identifier,
m.is_from_me,
m.text
FROM message m
LEFT JOIN handle h ON m.handle_id = h.ROWID
ORDER BY m.date;
The chat Table
One row per conversation thread.
- ROWID
: Primary key. - guid
: Unique identifier for the conversation. - chat_identifier
: For individual chats, the phone number or Apple ID. For group chats, a system-assigned group identifier. - room_name
: The group chat identifier — same value as cache_roomnamesin the message table. - display_name
: User-assigned name for group chats (if the user named the group). - is_archived
: Whether the conversation was archived.
The chat_message_join Table
This is the linking table between messages and chats. A message can theoretically appear in multiple conversations (edge cases with forwarding or syncing), so this is a many-to-many junction table.
- chat_id
: Foreign key to chat.ROWID - message_id
: Foreign key to message.ROWID
The chat_handle_join Table
Links handles (participants) to chat sessions. This is how you identify all participants in a group conversation.
- chat_id
: Foreign key to chat.ROWID - handle_id
: Foreign key to handle.ROWID
Core Data Timestamp Conversion: The Detail That Trips People Up
The date column in the message table stores seconds since January 1, 2001 — Apple's "Core Data" epoch. This is not the standard Unix epoch (January 1, 1970). The difference is 978,307,200 seconds.
To convert to UTC:
``
Unix timestamp = date column value + 978307200
In SQLite queries:
`sql`
datetime(date + 978307200, 'unixepoch') -- UTC
datetime(date + 978307200, 'unixepoch', 'localtime') -- Device local time
A critical nuance: iOS stores timestamps in the device's local time zone context, but the sms.db stores raw seconds without explicit timezone encoding. When you convert to "localtime" in SQLite, it uses your examination workstation's local time zone setting. If you're in Chicago examining a phone belonging to someone in San Diego, your "localtime" conversion is wrong by two hours.
Best practice: Convert to UTC for court exhibits. Always specify "UTC" explicitly in your reports and exhibits. Include the original raw timestamp value alongside the converted value so opposing counsel can verify your math.

Attachment Linking
Message attachments (photos, videos, audio, documents) are stored in a separate table and linked to messages.
The attachment Table
- ROWID
: Primary key. - filename
: The path to the attachment file on the device filesystem (relative to the home directory). - mime_type
: The file type. - transfer_name
: The original filename. - total_bytes
: File size. - created_date
: Core Data timestamp. - is_outgoing
: Whether this was a sent attachment.
The message_attachment_join Table
Links messages to their attachments (another many-to-many join, since a single message can have multiple attachments).
To pull all messages with their attachments:
`sql`
SELECT
m.ROWID AS message_id,
datetime(m.date + 978307200, 'unixepoch') AS message_date_utc,
h.id AS sender,
m.is_from_me,
m.text,
a.filename AS attachment_path,
a.mime_type,
a.transfer_name
FROM message m
LEFT JOIN handle h ON m.handle_id = h.ROWID
LEFT JOIN message_attachment_join maj ON m.ROWID = maj.message_id
LEFT JOIN attachment a ON maj.attachment_id = a.ROWID
ORDER BY m.date;
Group Chat Identification and Participant Resolution
Group chat evidence is where examiners most often get tripped up in depositions. Here's the correct methodology.
Step 1: Identify group conversations.
In the message table, group messages have a non-NULL cache_roomnames value. Individual messages have NULL.
Step 2: Find the chat record.
Match cache_roomnames to the room_name column in the chat table.
Step 3: Identify all participants.
Use chat_handle_join to get all handle_ids for that chat_id, then join to the handle table to get phone numbers/Apple IDs.
Step 4: Attribute individual messages within the group.
Within a group message, is_from_me = 1 means the device owner sent it. is_from_me = 0 means someone else sent it — and the handle_id tells you who.
The full participant query for a group chat:
`sql`
SELECT
h.id AS participant,
h.service
FROM chat_handle_join chj
JOIN handle h ON chj.handle_id = h.ROWID
JOIN chat c ON chj.chat_id = c.ROWID
WHERE c.room_name = 'chat[GROUP_ID]';
This produces a roster of everyone who was added to the group at any point — including people who later left. The join table accumulates history.
Tapbacks, Thread Replies, and Edited Messages
Modern iMessage features complicate court presentation. Here's how each appears in the database:
Tapbacks (reactions):
Reaction messages have a special associated_message_guid value and an associated_message_type value. Type 2000-2005 correspond to heart, thumbs up, thumbs down, ha ha, exclamation, and question mark. When presenting Tapback data in court, display the reaction alongside the original message with a clear visual connection — don't show them as standalone messages.
Thread replies:
The reply_to_guid column references the message being replied to. These display as threaded in the iMessage interface but are stored as individual messages. In exhibits, consider using an indented or visually connected presentation to preserve the threading context.
Edited messages:
iOS 16 introduced message editing. The current text of an edited message is stored in the text column. The edit history is stored separately in the message_edited_content table (available in more recent iOS/sms.db versions). Edited messages have an edited_parts flag. In court presentations, distinguish between original and edited text explicitly.
Deleted messages:
A deleted message may leave a tombstone record in some iOS versions — a message with NULL text and a deletion indicator. The SQLite free pages may contain recoverable text from the deleted record. Always examine free pages as part of a comprehensive examination.
Preparing Court Exhibits from sms.db
The transition from raw database to court-ready exhibit is where examiners lose the confidence of judges and juries. Practical guidance:
Use chronological order consistently. Sort by the date column, not ROWID. Present UTC timestamps, note the conversion methodology in your report.
Show sender identification clearly. For each message, the exhibit should clearly display whether it's from the device owner (is_from_me = 1) or from an identified contact (handle.id + display name if available).
Document the join methodology. In your expert report, include an appendix explaining the handle_id join. Opposing counsel's expert will verify this. If your methodology is sound, verification helps you.
Use screenshots from forensic tools as secondary exhibits. Cellebrite's formatted output, AXIOM's timeline view, and similar tool outputs are more readable than raw SQL exports. Include both the formatted exhibit and the underlying database query as a backup. The formatted exhibit is for the jury; the query methodology is for the expert examination.
Flag data anomalies. Gaps in message timestamps, unexpected handle_id values, messages with no corresponding chat record — document and explain anomalies rather than silently omitting them. Unexplained data gaps in court exhibits invite damaging cross-examination.
Chain of Custody for sms.db Specifically
If you're presenting sms.db data in court, your chain of custody documentation for that specific file needs to be airtight:
- Record the exact path from which the file was extracted
- Hash the file immediately post-extraction (SHA-256)
- Record the extraction tool, version, and methodology
- Note the device state (BFU/AFU) and iOS version
- Preserve the WAL file alongside the main database
- If you ran any SQL queries against the database, log every query
Under Daubert v. Merrell Dow Pharmaceuticals, Inc., 509 U.S. 579 (1993), your methodology must be documented, peer-reviewable, and based on accepted scientific methods. SQLite database forensics is well-established. The schema interpretation, however, is something you'll need to walk through in testimony.
FAQ
Q: The handle.id for some messages shows an email address, not a phone number. Why?
When iMessages are sent or received using an Apple ID email address (rather than a phone number), the handle.id stores the email address. This happens when the sender or recipient has their iMessage configured to use an email address as their caller ID, or when the message was sent to someone's Apple ID directly. This is not an anomaly — it's valid iMessage behavior. Cross-reference the email address with the device's Contacts app data to identify the person.
Q: Some messages have is_from_me = 1 but a non-zero handle_id. Is that an error?
Not necessarily. In some edge cases, particularly with synced messages across multiple Apple devices, the handle_id may be populated even for outgoing messages — it can reference the account the device owner used to send (their own Apple ID or phone number handle). Review the specific handle record to verify. If handle.id matches the device owner's identifiers, it's expected behavior.
Q: How do I handle messages where the text column is NULL but the message record exists?
NULL text occurs for several reasons: media-only messages (the text is NULL, the attachment exists), Tapback reactions (which are stored as messages with associated_message_type set), deleted messages where the content was wiped, and some system messages. Always join to the attachment table and check associated_message_type before characterizing a NULL-text message as empty or deleted.
Q: Can I tell from sms.db whether the recipient actually read the message?
The date_read column is populated when a message is marked as read on the receiving device. For sent messages (is_from_me = 1), a non-NULL date_read suggests the recipient opened the conversation. However, read receipts in iMessage are a user preference — the recipient can disable them. A NULL date_read` on a sent message does not confirm the message was unread; it may just mean read receipts were disabled.
Q: What’s the difference between the guid and the ROWID for messages?
The ROWID is a local, device-specific identifier that auto-increments. The guid is a globally unique identifier that theoretically stays consistent with the message across devices (e.g., in iCloud sync). For court presentation, ROWID is simpler to reference and explain. For cross-device correlation (comparing two parties’ databases), the guid is the right join key — though even guid matching is imperfect due to device-specific handling.