Data Schemas
Standardized schemas for storage in @arcaelas/whatsapp.
Principles: - Minimal: Only essential fields - Flat: No unnecessary nesting - Typed: Explicit types - Temporal: Timestamps in milliseconds UTC - Nullable: null for absence, never undefined
Raw Schemas
Classes work with "raw" objects that contain protocol properties. Getters/setters expose a simplified API.
Raw contact object properties (IContactRaw).
| Property | Type | Description |
id | string | Unique contact JID |
lid | string \| null | Alternative ID in LID format |
name | string \| null | Name saved in user's address book |
notify | string \| null | Profile push name set by the contact |
verifiedName | string \| null | Verified business account name |
imgUrl | string \| null | Profile picture URL (may expire or be "changed") |
status | string \| null | Contact profile bio/status |
Raw Example
{
"id": "584144709840@s.whatsapp.net",
"lid": "140913951141911@lid",
"name": "Juan Perez",
"notify": "Juanito",
"verifiedName": null,
"imgUrl": "https://pps.whatsapp.net/v/t61.24694-24/...",
"status": "Available 24/7"
}
Chat Raw
Raw conversation object properties (IChatRaw).
| Property | Type | Description |
id | string | Unique chat JID |
name | string \| null | Chat or group name |
displayName | string \| null | Alternative display name |
description | string \| null | Group description |
unreadCount | number \| null | Unread message count |
readOnly | boolean \| null | Whether the chat is read-only |
archived | boolean \| null | Whether the chat is archived |
pinned | number \| null | Pin timestamp |
muteEndTime | number \| null | Mute expiration timestamp |
markedAsUnread | boolean \| null | Whether manually marked as unread |
participant | IGroupParticipant[] \| null | Group participant list |
createdBy | string \| null | Group creator JID |
createdAt | number \| null | Creation timestamp |
ephemeralExpiration | number \| null | Ephemeral message duration (seconds) |
IGroupParticipant
| Property | Type | Description |
id | string | Participant JID |
admin | string \| null | Admin role: null, "admin", "superadmin" |
Raw Example
{
"id": "120363123456789@g.us",
"name": "Development Team",
"displayName": null,
"description": "Project coordination group",
"unreadCount": 5,
"readOnly": false,
"archived": false,
"pinned": 1767371367857,
"muteEndTime": null,
"markedAsUnread": false,
"createdAt": 1700000000,
"createdBy": "584144709840@s.whatsapp.net",
"participant": [
{ "id": "584144709840@s.whatsapp.net", "admin": "superadmin" },
{ "id": "584121234567@s.whatsapp.net", "admin": null }
],
"ephemeralExpiration": 604800
}
Message Raw
Raw message object properties. The library stores two parts: IMessageIndex (metadata) and WAMessage (full Baileys raw).
MessageKey (key)
| Property | Type | Description |
key.remoteJid | string \| null | Chat JID |
key.fromMe | boolean \| null | Whether the message is own |
key.id | string \| null | Unique message ID |
key.participant | string \| null | Sender JID (in groups) |
Main Properties
| Property | Type | Description |
message | Message \| null | Message content (see Message Content) |
messageTimestamp | number \| null | Creation timestamp |
status | MessageStatus \| null | Delivery status (0-5) |
pushName | string \| null | Sender name |
broadcast | boolean \| null | Whether it's a broadcast message |
starred | boolean \| null | Whether it's starred |
duration | number \| null | Duration in seconds (audio/video) |
labels | string[] \| null | Message labels |
Message Statuses (status)
| Value | Constant | Description |
0 | ERROR | Send error |
1 | PENDING | Pending send |
2 | SERVER_ACK | Server acknowledged |
3 | DELIVERED | Delivered to recipient |
4 | READ | Read by recipient |
5 | PLAYED | Played (audio/video) |
| Property | Type | Description |
mediaCiphertextSha256 | Uint8Array \| null | SHA256 hash of encrypted media |
mediaData | MediaData \| null | Media data |
quotedStickerData | MediaData \| null | Quoted sticker data |
Temporal Properties
| Property | Type | Description |
ephemeralStartTimestamp | number \| null | Expiration timer start |
ephemeralDuration | number \| null | Duration until expiration (seconds) |
ephemeralOffToOn | boolean \| null | Whether changed from off to on |
ephemeralOutOfSync | boolean \| null | Whether out of sync |
revokeMessageTimestamp | number \| null | Revocation timestamp |
Reaction and Poll Properties
| Property | Type | Description |
reactions | Reaction[] \| null | Message reactions |
pollUpdates | PollUpdate[] \| null | Poll updates |
pollAdditionalMetadata | PollAdditionalMetadata \| null | Additional poll metadata |
messageSecret | Uint8Array \| null | Secret for decrypting votes |
Receipt Properties
| Property | Type | Description |
userReceipt | UserReceipt[] \| null | Per-user read receipts |
messageC2STimestamp | number \| null | Client-to-server timestamp |
Stub Properties (System Messages)
| Property | Type | Description |
messageStubType | StubType \| null | System message type |
messageStubParameters | string[] \| null | Stub parameters |
Business Properties
| Property | Type | Description |
bizPrivacyStatus | BizPrivacyStatus \| null | Business privacy status |
verifiedBizName | string \| null | Verified business name |
Status/History Properties
| Property | Type | Description |
statusPsa | StatusPSA \| null | Status PSA |
statusAlreadyViewed | boolean \| null | Whether status was already viewed |
isMentionedInStatus | boolean \| null | Whether mentioned in status |
statusMentions | string[] \| null | JIDs mentioned in status |
statusMentionMessageInfo | StatusMentionMessage \| null | Status mention message info |
statusMentionSources | string[] \| null | Mention sources |
Pin Properties
| Property | Type | Description |
pinInChat | PinInChat \| null | Pinned message info |
keepInChat | KeepInChat \| null | Keep in chat |
Bot/AI Properties
| Property | Type | Description |
is1PBizBotMessage | boolean \| null | Whether it's a 1P business bot message |
botMessageInvokerJid | string \| null | JID that invoked the bot |
botTargetId | string \| null | Bot target ID |
isSupportAiMessage | boolean \| null | Whether it's a support AI message |
supportAiCitations | Citation[] \| null | AI citations |
agentId | string \| null | Agent ID |
Internal Properties
| Property | Type | Description |
ignore | boolean \| null | Whether to ignore |
multicast | boolean \| null | Whether multicast |
urlText | boolean \| null | Whether contains text URL |
urlNumber | boolean \| null | Whether contains number URL |
clearMedia | boolean \| null | Whether to clear media |
photoChange | PhotoChange \| null | Photo change |
futureproofData | Uint8Array \| null | Future compatibility data |
isGroupHistoryMessage | boolean \| null | Whether group history message |
originalSelfAuthorUserJidString | string \| null | Original author JID |
premiumMessageInfo | PremiumMessageInfo \| null | Premium message info |
commentMetadata | CommentMetadata \| null | Comment metadata |
eventResponses | EventResponse[] \| null | Event responses |
eventAdditionalMetadata | EventAdditionalMetadata \| null | Additional event metadata |
reportingTokenInfo | ReportingTokenInfo \| null | Reporting token info |
newsletterServerId | number \| null | Newsletter server ID |
targetMessageId | MessageKey \| null | Target message ID |
messageAddOns | MessageAddOn[] \| null | Message add-ons |
paymentInfo | PaymentInfo \| null | Payment info |
quotedPaymentInfo | PaymentInfo \| null | Quoted payment info |
finalLiveLocation | LiveLocationMessage \| null | Final live location |
Raw Example
{
"key": {
"remoteJid": "584144709840@s.whatsapp.net",
"fromMe": false,
"id": "AC07DE0D18FA8254897A26C90B2FFD98",
"participant": null
},
"message": {
"extendedTextMessage": {
"text": "Hello world",
"contextInfo": {
"stanzaId": "AC9BE0D81D965A3C240B6ACAA891C6FD",
"participant": "584121234567@s.whatsapp.net",
"isForwarded": false,
"forwardingScore": 0,
"expiration": 604800
}
}
},
"messageTimestamp": 1767366759,
"status": 4,
"pushName": "Juan Perez",
"broadcast": false,
"starred": false,
"duration": null,
"labels": [],
"ephemeralStartTimestamp": 1767366759,
"ephemeralDuration": 604800,
"reactions": [
{ "text": "❤️", "groupingKey": "❤️", "senderTimestampMs": 1767366800000 }
],
"userReceipt": [
{ "userJid": "584144709840@s.whatsapp.net", "readTimestamp": 1767366800 }
]
}
Message Content
Content types inside message.
Text
| Property | Type | Description |
conversation | string | Simple text |
extendedTextMessage.text | string | Text with metadata |
extendedTextMessage.contextInfo | ContextInfo | Context information |
| Property | Type | Description |
imageMessage | ImageMessage | Image message |
videoMessage | VideoMessage | Video message |
audioMessage | AudioMessage | Audio message |
documentMessage | DocumentMessage | Document message |
stickerMessage | StickerMessage | Sticker message |
| Property | Type | Description |
url | string | Download URL (temporary) |
directPath | string | Direct CDN path |
mediaKey | Uint8Array | Decryption key |
mimetype | string | MIME type |
fileLength | number | Size in bytes |
fileSha256 | Uint8Array | File SHA256 hash |
fileEncSha256 | Uint8Array | Encrypted SHA256 hash |
mediaKeyTimestamp | number | Key timestamp |
jpegThumbnail | Uint8Array | JPEG thumbnail in base64 |
caption | string | Media caption |
width | number | Width in pixels |
height | number | Height in pixels |
seconds | number | Duration (audio/video) |
ptt | boolean | Push-to-talk (voice note) |
waveform | Uint8Array | Audio waveform |
streamingSidecar | Uint8Array | Streaming data |
Location
| Property | Type | Description |
locationMessage.degreesLatitude | number | Latitude |
locationMessage.degreesLongitude | number | Longitude |
liveLocationMessage.degreesLatitude | number | Live latitude |
liveLocationMessage.degreesLongitude | number | Live longitude |
liveLocationMessage.sequenceNumber | number | Sequence number |
liveLocationMessage.caption | string | Location caption |
Poll
| Property | Type | Description |
pollCreationMessage.name | string | Poll question |
pollCreationMessage.options | Option[] | Answer options |
pollCreationMessage.selectableOptionsCount | number | Selectable count |
pollUpdateMessage.pollCreationMessageKey | MessageKey | Poll reference |
pollUpdateMessage.vote | PollEncValue | Encrypted vote |
Reaction
| Property | Type | Description |
reactionMessage.key | MessageKey | Message being reacted to |
reactionMessage.text | string | Reaction emoji |
reactionMessage.senderTimestampMs | number | Sender timestamp |
Protocol
| Property | Type | Description |
protocolMessage.type | ProtocolMessageType | Protocol type |
protocolMessage.key | MessageKey | Target message |
protocolMessage.editedMessage | Message | Edited message |
protocolMessage.timestampMs | number | Action timestamp |
ProtocolMessage Types
| Value | Constant | Description |
0 | REVOKE | Delete message |
3 | EPHEMERAL_SETTING | Configure ephemeral messages |
6 | EPHEMERAL_SYNC_RESPONSE | Sync response |
7 | HISTORY_SYNC_NOTIFICATION | History notification |
14 | MESSAGE_EDIT | Edit message |
ContextInfo
| Property | Type | Description |
stanzaId | string | Quoted message ID |
participant | string | Quoted message author |
quotedMessage | Message | Quoted message content |
isForwarded | boolean | Whether forwarded |
forwardingScore | number | Times forwarded |
expiration | number | Seconds until expiration |
ephemeralSettingTimestamp | number | Configuration timestamp |
mentionedJid | string[] | Mentioned JIDs |
API Schemas
Simplified schemas exposed by classes.
| Property | Type | Raw Source | Description |
id | string | id | Unique contact JID |
name | string | name \|\| notify \|\| id.split("@")[0] | Contact name |
photo | string \| null | imgUrl | Profile picture URL |
phone | string | id.split("@")[0] | Phone number |
content | string | status \|\| "" | Contact bio |
Storage Key
Stored format: IContactRaw JSON (id, lid, name, notify, verifiedName, imgUrl, status).
Chat
| Property | Type | Raw Source | Description |
id | string | id | Unique chat JID |
type | "contact" \| "group" | Derived from id | Chat type |
name | string | name \|\| displayName \|\| id.split("@")[0] | Chat name |
content | string | description \|\| "" | Description |
pinned | boolean | pinned !== null | Whether pinned |
archived | boolean | archived \|\| false | Whether archived |
muted | number | muteEndTime \|\| 0 | Mute end timestamp (0 if unmuted) |
read | boolean | unreadCount === 0 && !markedAsUnread | Whether read |
readonly | boolean | readOnly \|\| false | Whether read-only |
Storage Key
Stored format: IChatRaw JSON (id, name, displayName, description, unreadCount, readOnly, archived, pinned, muteEndTime, markedAsUnread, participant, createdBy, createdAt, ephemeralExpiration).
Message
IMessageIndex (stored in chat/{cid}/message/{id}/index)
| Property | Type | Raw Source | Description |
id | string | key.id | Unique ID |
cid | string | key.remoteJidAlt \|\| key.remoteJid | Chat ID |
mid | string \| null | contextInfo.stanzaId | Parent message |
me | boolean | key.fromMe | Whether own message |
type | MessageType | Derived from message | Message type |
author | string | key.participant \|\| key.remoteJid (uses \|\| not ??) | Author |
status | MESSAGE_STATUS | status | Delivery status |
starred | boolean | starred | Whether starred |
forwarded | boolean | contextInfo.isForwarded | Whether forwarded |
created_at | number | messageTimestamp * 1000 | Creation (ms) |
deleted_at | number \| null | ephemeralStartTimestamp + ephemeralDuration | Expiration |
mime | string | Derived from message | MIME type |
caption | string | *.caption \|\| "" | Caption |
edited | boolean | Set on edit | Whether edited |
Instance methods
| Method | Return Type | Description |
content() | Promise<Buffer> | Gets message content as Buffer (async) |
stream() | Promise<Readable> | Gets content as Readable stream (async) |
react(emoji) | Promise<boolean> | Reacts to the message |
edit(text) | Promise<boolean> | Edits the message (own messages only) |
remove() | Promise<boolean> | Deletes the message for everyone |
forward(to_cid) | Promise<boolean> | Forwards to another chat |
text(content) | Promise<Message \| null> | Replies with text |
image(buffer, caption?) | Promise<Message \| null> | Replies with image |
video(buffer, caption?) | Promise<Message \| null> | Replies with video |
audio(buffer, ptt?) | Promise<Message \| null> | Replies with audio |
location(opts) | Promise<Message \| null> | Replies with location |
poll(opts) | Promise<Message \| null> | Replies with poll |
Storage
| Key | Content |
chat/{cid}/message/{id}/index | JSON with IMessageIndex metadata |
chat/{cid}/message/{id}/content | Buffer base64 |
chat/{cid}/message/{id}/raw | Full WAMessage raw (for forward, re-download) |
Field Summary
| Entity | API Getters | Raw Fields (IChatRaw/IContactRaw) |
| Contact | 5 (id, name, photo, phone, content) | 7 (id, lid, name, notify, verifiedName, imgUrl, status) |
| Chat | 9 (id, type, name, content, pinned, archived, muted, read, readonly) | 14 |
| Message | 14 index fields + async methods | WAMessage (50+) |
Storage Guidelines
Storage strategies for different backends.
Primary Fields (Columns/Indexes)
Scalar fields used for filters/searches:
| Entity | Primary Fields |
| Contact | id |
| Chat | id, archived, pinned, muteEndTime |
| Message | id, cid, me, author, status, created_at, starred |
NoSQL Strategy (Key-Value)
For FileEngine, RedisEngine, or other key-value stores.
Key Structure
contact/{id}/index -> IContactRaw JSON
lid/{lid} -> JID string
chat/{id}/index -> IChatRaw JSON
chat/{cid}/messages -> Message index text file
chat/{cid}/message/{id}/index -> IMessageIndex JSON
chat/{cid}/message/{id}/content -> Buffer base64
chat/{cid}/message/{id}/raw -> WAMessage JSON
// contact/{id}/index -- stores IContactRaw
{
"id": "584144709840@s.whatsapp.net",
"lid": "140913951141911@lid",
"name": "Juan Perez",
"notify": "Juanito",
"verifiedName": null,
"imgUrl": "https://...",
"status": "Available"
}
// chat/{id}/index -- stores IChatRaw
{
"id": "120363123456789@g.us",
"name": "Dev Team",
"displayName": null,
"description": "Group description",
"unreadCount": 5,
"readOnly": false,
"archived": false,
"pinned": 1767371367857,
"muteEndTime": null,
"markedAsUnread": false,
"participant": [
{ "id": "584144709840@s.whatsapp.net", "admin": "superadmin" }
],
"createdBy": "584144709840@s.whatsapp.net",
"createdAt": 1700000000,
"ephemeralExpiration": 604800
}
// chat/{cid}/message/{id}/index -- stores IMessageIndex
{
"id": "AC07DE0D18FA8254897A26C90B2FFD98",
"cid": "584144709840@s.whatsapp.net",
"mid": null,
"me": false,
"type": "text",
"author": "584144709840@s.whatsapp.net",
"status": 4,
"starred": false,
"forwarded": false,
"created_at": 1767366759000,
"deleted_at": null,
"mime": "text/plain",
"caption": "",
"edited": false
}
Advantages
- Simplicity: Everything is JSON, no migrations
- Atomicity: One record = one key
- Flexibility: Add fields without altering structure
Considerations
Listings and Filters:
# List contacts
engine.list("contact/")
# List messages of a chat
Read chat/{cid}/messages index, then fetch each message
# Filter starred messages
Requires iterating and filtering in memory
Cleanup (Cascade Delete):
# Delete chat and all its messages
engine.set("chat/{cid}", null)
# The engine cascade-deletes all sub-keys: chat/{cid}/*
Orphan Data: - When deleting chat: set("chat/{cid}", null) removes all keys with prefix chat/{cid}/ - When deleting contact: set("contact/{id}/index", null) (no dependencies)
SQL Strategy (Relational)
For PostgreSQL, MySQL, SQLite.
Table Schema
-- Contacts (stores IContactRaw fields)
CREATE TABLE contacts (
id VARCHAR(50) PRIMARY KEY,
lid VARCHAR(50),
name VARCHAR(255),
notify VARCHAR(255),
verified_name VARCHAR(255),
img_url TEXT,
status TEXT
);
-- Chats (stores IChatRaw fields)
CREATE TABLE chats (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255),
display_name VARCHAR(255),
description TEXT,
unread_count INTEGER,
read_only BOOLEAN DEFAULT FALSE,
archived BOOLEAN DEFAULT FALSE,
pinned BIGINT, -- timestamp or NULL
mute_end_time BIGINT, -- timestamp or NULL
marked_as_unread BOOLEAN DEFAULT FALSE,
created_by VARCHAR(50),
created_at BIGINT,
ephemeral_expiration INTEGER
);
-- Group participants
CREATE TABLE chat_participants (
chat_id VARCHAR(50) REFERENCES chats(id) ON DELETE CASCADE,
contact_id VARCHAR(50),
admin VARCHAR(20), -- NULL, 'admin', 'superadmin'
PRIMARY KEY (chat_id, contact_id)
);
-- Messages (stores IMessageIndex fields)
CREATE TABLE messages (
id VARCHAR(50),
cid VARCHAR(50) REFERENCES chats(id) ON DELETE CASCADE,
mid VARCHAR(50), -- parent message (reply)
me BOOLEAN NOT NULL,
type VARCHAR(20) NOT NULL,
author VARCHAR(50) NOT NULL,
status SMALLINT DEFAULT 1,
starred BOOLEAN DEFAULT FALSE,
forwarded BOOLEAN DEFAULT FALSE,
created_at BIGINT NOT NULL,
deleted_at BIGINT,
mime VARCHAR(100),
caption TEXT,
edited BOOLEAN DEFAULT FALSE,
PRIMARY KEY (cid, id)
);
-- Message content (separated for efficiency)
CREATE TABLE message_contents (
cid VARCHAR(50),
message_id VARCHAR(50),
content BYTEA,
raw JSONB,
PRIMARY KEY (cid, message_id),
FOREIGN KEY (cid, message_id) REFERENCES messages(cid, id) ON DELETE CASCADE
);
-- Reactions
CREATE TABLE message_reactions (
cid VARCHAR(50),
message_id VARCHAR(50),
reactor_id VARCHAR(50),
emoji VARCHAR(10),
timestamp BIGINT,
PRIMARY KEY (cid, message_id, reactor_id),
FOREIGN KEY (cid, message_id) REFERENCES messages(cid, id) ON DELETE CASCADE
);
Recommended Indexes
-- Frequent searches
CREATE INDEX idx_messages_chat ON messages(cid);
CREATE INDEX idx_messages_created ON messages(cid, created_at DESC);
CREATE INDEX idx_messages_author ON messages(author);
CREATE INDEX idx_messages_starred ON messages(cid, starred) WHERE starred = TRUE;
-- Chat filters
CREATE INDEX idx_chats_archived ON chats(archived) WHERE archived = TRUE;
CREATE INDEX idx_chats_pinned ON chats(pinned) WHERE pinned IS NOT NULL;
Advantages
- Integrity: Foreign keys guarantee consistency
- Cascade Delete:
ON DELETE CASCADE cleans automatically - Efficient Filters: Indexes on search fields
- Joins: Relate data without multiple queries
Common Queries
-- Chat messages sorted
SELECT * FROM messages WHERE cid = ? ORDER BY created_at DESC LIMIT 50;
-- Starred messages from user
SELECT * FROM messages WHERE me = TRUE AND starred = TRUE;
-- Chats with unread messages
SELECT * FROM chats WHERE unread_count > 0 OR marked_as_unread = TRUE;
-- Delete chat (cascade deletes messages, reactions, etc.)
DELETE FROM chats WHERE id = ?;
Comparison
| Aspect | NoSQL (Key-Value) | SQL (Relational) |
| Setup | Minimal | Requires migrations |
| Filters | In memory | Native indexes |
| Joins | N queries | 1 query |
| Cascade Delete | Via set(key, null) | Automatic |
| Schema Flexibility | High | Requires ALTER |
| Orphan Data | Possible if contract not followed | Impossible |
| Scalability | Horizontal | Vertical |
Recommendations by Use Case
Simple Bot / Prototype
- Backend: FileEngine (JSON files)
- Reason: Zero setup, easy debugging
Production Bot (< 100k messages)
- Backend: SQLite + FileEngine (content)
- Reason: Balance between performance and simplicity
Production Bot (> 100k messages)
- Backend: PostgreSQL + Redis (cache)
- Reason: Indexes, cascade delete, complex queries
Multi-tenant / SaaS
- Backend: PostgreSQL with partitioning by
cid - Reason: Data isolation, per-tenant cleanup
Orphan Data Prevention
Principle
When deleting a parent entity, ALWAYS delete its dependencies.
Deletion Cascade
Delete Contact:
+-- contact/{id}/index
Delete Chat (via set("chat/{cid}", null)):
|-- chat/{id}/index
|-- chat/{id}/messages
|-- chat/{id}/message/*/index
|-- chat/{id}/message/*/content
|-- chat/{id}/message/*/raw
+-- (SQL) chat_participants, messages, message_contents, message_reactions
Delete Message (via set("chat/{cid}/message/{mid}", null)):
|-- chat/{cid}/message/{id}/index
|-- chat/{cid}/message/{id}/content
+-- chat/{cid}/message/{id}/raw
(SQL) message_contents, message_reactions
Implementation
Cascade delete is handled by the Engine's set(key, null) contract. The library calls:
// Delete chat and all its content
await wa.Chat.remove(cid);
// or
const chat = await wa.Chat.get(cid);
await chat.remove();
// Internally calls:
// await wa.engine.set("chat/{cid}", null);
// which cascade-deletes all sub-keys