-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Description
Summary
The Contact.pushName field in the database is being silently overwritten with an empty string "" every time the instance owner sends a message. This causes contact names to disappear across all consumers (API responses, frontend, integrations).
The root cause is in whatsapp.baileys.service.ts — three code paths write to Contact.pushName without any guard against empty/undefined values, even though a proper guard already exists for Chat.name updates (line 1187).
Affected versions: v2.3.7 (and likely all v2.x versions)
Related issue: #2004
The Problem
What users see
POST /chat/findContacts/{instance}returnspushName: ""for most contactsPOST /chat/findChats/{instance}returns empty names (compounded by the duplicate column bug in empty pushName returned by {{baseUrl}}/chat/findChats/ and {{baseUrl}}/chat/findContacts/ #2004)- Contact names disappear over time — the more messages you send, the more names vanish
What happens in the database
Given a contact "Maria Santos" (551199*****2@s.whatsapp.net) across 3 instances:
| Instance | Contact.pushName | Explanation |
|---|---|---|
| Instance A | "Maria Santos" |
Last operation was a received message |
| Instance B | "" |
Last operation was a sent message → pushName overwritten with "" |
| Instance C | "" |
Same — sent message wiped the name |
The Message.pushName field on individual received messages still contains "Maria Santos", proving the data exists — it's just being discarded during the Contact upsert.
Root Cause Analysis
There are 3 bugs in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts, all related to missing validation before writing pushName to the Contact table.
Bug 1 (Critical): Sent messages overwrite contact pushName with empty string
File: whatsapp.baileys.service.ts, line 1503
const contactRaw = {
remoteJid: received.key.remoteJid,
pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName,
// ...
};When fromMe = true, pushName is explicitly set to ''. This contactRaw is then passed to:
// Line 1535 (existing contact) and line 1547 (new contact)
await this.prismaRepository.contact.upsert({
where: { remoteJid_instanceId: { ... } },
create: contactRaw,
update: contactRaw, // ← overwrites Contact.pushName with ""
});Impact: Every single sent message overwrites the recipient's Contact.pushName with an empty string. This is the most damaging bug — it means active conversations (where you send messages frequently) are the ones most likely to lose contact names.
Bug 2: contacts.update handler ignores the notify field from Baileys
File: whatsapp.baileys.service.ts, line 903
pushName: contact?.name ?? contact?.verifiedName,Baileys stores pushName in the notify field of its Contact type, not in name:
// Baileys Contact interface (src/Types/Contact.ts)
interface Contact {
name?: string // Name YOU saved in YOUR contacts (locally)
notify?: string // Name the CONTACT set on THEIR profile (= pushName)
}When Baileys emits contacts.update after receiving a message with pushName, it sends:
{ id: jid, notify: "Maria Santos", verifiedName: "" }But the code reads contact.name (which is undefined in this event) and falls back to contact.verifiedName (often ""). The notify field — where the actual pushName lives — is never read.
Additionally, ?? (nullish coalescing) is used instead of || (logical OR), so an empty string "" from contact.name would NOT fall through to verifiedName.
Bug 3: No guard against overwriting valid pushName with empty values
Lines 1535 and 1547 pass the full contactRaw object to both create and update in prisma.contact.upsert(). There is no check like "only update pushName if the new value is non-empty".
Ironically, this guard already exists for Chat.name updates (line 1183-1190):
if (
existingChat &&
received.pushName &&
existingChat.name !== received.pushName &&
received.pushName.trim().length > 0 && // ← THIS GUARD
!received.key.fromMe && // ← AND THIS ONE
!received.key.remoteJid.includes('@g.us')
) {
await this.prismaRepository.chat.update({
where: { id: existingChat.id },
data: { name: received.pushName },
});
}This guard correctly:
- Rejects empty/whitespace pushName values
- Skips updates from sent messages (
fromMe)
But the equivalent Contact writes at lines 1535 and 1547 have none of these protections.
Data Flow Diagram
WhatsApp message received with pushName "John"
│
├─ Baileys emits contacts.update: { id: jid, notify: "John" }
│ │
│ └─ Evolution reads: contact.name ?? contact.verifiedName
│ = undefined ?? undefined = undefined
│ ❌ "John" (in contact.notify) is NEVER read
│
└─ Baileys emits messages.upsert: { pushName: "John", key: { fromMe: false } }
│
└─ contactRaw.pushName = "John" ✅ (correct for received messages)
prisma.contact.upsert({ update: contactRaw }) → saves "John"
Instance owner SENDS a message to John
│
└─ Baileys emits messages.upsert: { key: { fromMe: true } }
│
└─ contactRaw.pushName = '' ❌ (hardcoded empty for fromMe)
prisma.contact.upsert({ update: contactRaw }) → overwrites "John" with ""
Proposed Fix
Fix 1: Guard contact upsert in messages.upsert handler
Lines 1535-1539 and 1547-1551
Only include pushName in the update payload when it has a meaningful value:
// Build update data — never overwrite a valid pushName with empty
const contactUpdate: any = {
remoteJid: contactRaw.remoteJid,
profilePicUrl: contactRaw.profilePicUrl,
instanceId: contactRaw.instanceId,
};
// Only update pushName if we have a real value (not empty, not just a phone number pattern)
if (contactRaw.pushName && contactRaw.pushName.trim().length > 0) {
contactUpdate.pushName = contactRaw.pushName;
}
await this.prismaRepository.contact.upsert({
where: { remoteJid_instanceId: { remoteJid: contactRaw.remoteJid, instanceId: contactRaw.instanceId } },
create: contactRaw, // For new contacts, use whatever data we have
update: contactUpdate, // For existing contacts, preserve pushName if new value is empty
});Fix 2: Read notify field in contacts.update handler
Line 903
// Before (bug):
pushName: contact?.name ?? contact?.verifiedName,
// After (fix):
pushName: contact?.notify || contact?.name || contact?.verifiedName,Use || instead of ?? so empty strings fall through, and read notify first (where Baileys actually stores pushName from incoming messages).
Fix 3: Apply the same guard pattern to contacts.update upsert
Lines 913-918
const updateTransactions = contactsRaw.map((contact) => {
const updateData: any = {
remoteJid: contact.remoteJid,
profilePicUrl: contact.profilePicUrl,
instanceId: contact.instanceId,
};
if (contact.pushName && contact.pushName.trim().length > 0) {
updateData.pushName = contact.pushName;
}
return this.prismaRepository.contact.upsert({
where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } },
create: contact,
update: updateData,
});
});Why This Hasn't Been Caught Earlier
-
Message.pushName still works: Individual messages store
pushNamecorrectly (viaprepareMessage()which uses||at line 4652). So message display in chat views appears fine. -
The Chat.name guard masks the issue: Chat names are protected by the guard at line 1187, so the Chat table retains names even as the Contact table loses them.
-
Initial sync uses
createManywithskipDuplicates: Thecontacts.upserthandler (line 822) correctly usesskipDuplicates: true, so it never overwrites — but this only runs on initial connection, not on message-triggered updates. -
The damage is gradual: pushName is overwritten one contact at a time, each time a message is sent. It's not immediately obvious unless you compare Contact table data across time.
Verification Query
Run this against your PostgreSQL database to see the impact:
-- Count contacts with empty pushName vs total
SELECT
i.name as instance,
COUNT(*) as total_contacts,
COUNT(*) FILTER (WHERE c."pushName" IS NULL OR c."pushName" = '') as empty_pushname,
ROUND(100.0 * COUNT(*) FILTER (WHERE c."pushName" IS NULL OR c."pushName" = '') / COUNT(*), 1) as pct_empty
FROM "Contact" c
JOIN "Instance" i ON i.id = c."instanceId"
GROUP BY i.name
ORDER BY i.name;-- Find contacts where Message.pushName exists but Contact.pushName is empty
SELECT c."remoteJid", c."pushName" as contact_pushname, m."pushName" as message_pushname, i.name as instance
FROM "Contact" c
JOIN "Instance" i ON i.id = c."instanceId"
JOIN LATERAL (
SELECT "pushName" FROM "Message" m
WHERE m."instanceId" = c."instanceId"
AND m.key->>'remoteJid' = c."remoteJid"
AND (m.key->>'fromMe')::boolean = false
AND m."pushName" IS NOT NULL AND m."pushName" != ''
ORDER BY "messageTimestamp" DESC LIMIT 1
) m ON true
WHERE c."pushName" IS NULL OR c."pushName" = ''
ORDER BY i.name, c."remoteJid";Real-World Impact (Production Data)
Affected contacts per instance
| Instance | Total Contacts | Empty pushName | % Affected |
|---|---|---|---|
| Instance A | ~6,300 | 22 | 0.4% |
| Instance B | ~6,300 | 133 | 2.1% |
| Instance C | ~6,200 | 105 | 1.7% |
The instances with more sent messages (B and C) have significantly more affected contacts, confirming that outgoing messages are the primary cause of data loss.
Proof: Contact.pushName is empty but Message.pushName has the real name
These are contacts where Contact.pushName = "" but the actual name exists in their received messages:
| Contact JID (masked) | Contact.pushName | Message.pushName (from received msgs) |
|---|---|---|
5511*****022@s.whatsapp.net |
"" |
"Maria S." |
5511*****929@s.whatsapp.net |
"" |
"Carlos F." |
5511*****488@s.whatsapp.net |
"" |
"Ana T." |
5511*****316@s.whatsapp.net |
"" |
"Pedro L." |
5511*****465@s.whatsapp.net |
"" |
"Juliana R." |
In all cases, the real name is present in Message.pushName from received messages — proving the data was correctly captured by Baileys but then overwritten with "" when the instance owner sent a reply.
Environment
- Evolution API version: 2.3.7
- Database provider: PostgreSQL
- Baileys version: as bundled with v2.3.7
DATABASE_SAVE_DATA_CONTACTS:true
Workaround
Setting DATABASE_SAVE_DATA_CONTACTS=false freezes the Contact table (stops all writes) while webhooks and message saving continue to work normally. This prevents further data loss but also prevents new contacts from being created. It's a viable temporary measure until the fix is applied.