Spec: WhatsApp Phone Number on Patient Profile
Date: 2026-04-28Status: Approved
Summary
Add a dedicatedwhatsapp_phone field to each patient record so outbound WhatsApp appointment reminders go to the correct number. The feature is only visible to clinics with the whatsapp_api addon enabled. Both phone fields get a country-flag + dial-code picker using react-international-phone.
Addon Gate
The entire WhatsApp phone UI is hidden unless the clinic has thewhatsapp_api module enabled.
- If the addon is off, the patient form shows only the standard phone field (unchanged).
- If the addon is on, the “Also on WhatsApp” toggle and the separate WhatsApp number field become visible.
- Gate is enforced in the frontend via the existing
useClinicModules/ module-check pattern. No server-side enforcement needed beyond existing permission checks.
Database
New nullable column onpatients:
whatsapp_phone— nullable text, E.164 format, encrypted at rest using the same AES-256-GCM path asphone.- Existing rows are unaffected;
whatsapp_phone = NULLmeans no WhatsApp number on file. - No migration needed for existing patients — WhatsApp sends are silently skipped when
NULL.
Phone Input Component
Library:react-international-phone
- Flag picker + searchable country dropdown (all countries).
- Stores value in E.164 (
+971501234567). - Lightweight, TypeScript, styleable with CSS variables.
- Applied to both the primary phone field and the WhatsApp number field.
- Dark-theme styling to match existing inputs (border
#333, background#1a1a1a, text#e0e0e0).
PhoneInput wrapper component will be created at ui/src/components/ui/PhoneInput.tsx to apply dark-theme CSS overrides and consistent props to both fields.
Patient Form Behaviour (addon ON)
Primary phone field
- Replaced with
<PhoneInput>(flag + E.164 input). - Still required (
NOT NULL). - Next to the field: a green “Also on WhatsApp” badge toggle.
- Default state: ON (green, checked) for new patients — most patients share one number.
- When ON:
whatsapp_phoneis saved as the same value asphone. No second field shown. - When toggled OFF: the badge goes grey and a second WhatsApp field appears below.
WhatsApp number field (shown only when toggle is OFF)
- Same
<PhoneInput>component. - Label: WhatsApp icon + “Different WhatsApp Number (optional)”.
- Optional — may be left blank. If blank and toggle is OFF,
whatsapp_phoneis saved asNULL. - Placeholder: “Only fill if different from above”.
Edit mode (existing patients)
- If
whatsapp_phone === phone→ toggle starts ON, no second field. - If
whatsapp_phoneis a different number → toggle starts OFF, second field pre-filled. - If
whatsapp_phone === NULL→ toggle starts OFF, second field empty.
Patient Detail View
- Phone row: existing, unchanged.
- WhatsApp row: shown only if
whatsapp_phoneis non-null AND clinic has addon enabled.- Label: WhatsApp SVG icon + “WhatsApp”.
- Value: formatted number (display as-is from DB — already E.164).
- No
wa.melink — plain text only. - If
whatsapp_phoneis null → row hidden entirely.
Backend: WhatsApp Send Logic
Appointment reminders (appointment-reminders.ts)
Incoming webhook cancel flow (whatsapp-webhook.ts)
Match incoming message sender in this order:
patients.whatsapp_phonefirstpatients.phonesecond (patient may have texted in from their regular number)
Outbound messages route (messages.ts)
Same rule: use patient.whatsapp_phone if set, otherwise skip.
Decryption / API
decryptPatientPHI must include whatsapp_phone in the decrypted fields returned to the frontend (same encryption path as phone). The API response shape for patient records gains:
What Is NOT Changing
phonestays required — no migration for existing patients.- Patient list view unchanged.
- No WhatsApp links anywhere in the UI.
- Clinics without the
whatsapp_apiaddon see no new UI at all — their patient form is identical to today.
Files Affected
| File | Change |
|---|---|
server/src/schema/patients.ts | Add whatsapp_phone nullable column |
server/drizzle/ | New migration file |
server/src/lib/encryption.ts (or PHI utils) | Include whatsapp_phone in encrypt/decrypt |
server/src/routes/patients.ts | Read/write whatsapp_phone; return in API response |
server/src/scheduled/appointment-reminders.ts | Use whatsappPhone for WA sends |
server/src/routes/whatsapp-webhook.ts | Match incoming by whatsapp_phone first |
server/src/routes/messages.ts | Use whatsappPhone for outbound |
ui/src/components/ui/PhoneInput.tsx | New wrapper around react-international-phone |
ui/src/components/patients/PatientForm.tsx | Toggle + conditional WhatsApp field; addon gate |
ui/src/components/patients/PatientDetails.tsx | WhatsApp row; hide if null; addon gate |
ui/package.json | Add react-international-phone |

