I recently joined as a part-time business advisor for a new skin clinic in Bangalore, India. Like most early-stage clinics, nothing was fundamentally broken, but everything had friction. Appointments were tracked in Google Sheets, reminders were sent manually on WhatsApp, and follow-ups depended heavily on memory and verbal handovers. When things got busy, reminders slipped, patients missed appointments, and the staff ended up firefighting instead of focusing on patient conversations and sales.
For this problem, I just thought this could be solved easily to avoid loss to the business. As an engineer, I wasn't trying to introduce heavy software or replace tools people were already comfortable with. The goal was simpler: remove communication errors, reduce cognitive load, and let computers do repetitive work reliably while humans focused on people.
This post walks through the system I built to make that happen.
The Core Idea
The clinic already trusted Google Sheets, so I let that remain the primary input. PostgreSQL became the system of record, responsible for answering questions like "who needs a reminder tomorrow?" and "which treatments are still active?". n8n sat in the middle, orchestrating data movement and automation, and WhatsApp became the delivery channel using the whatsapp-web.js API.
Nothing fancy. Just clear ownership of responsibility.
Design Philosophy
Before writing any code, I had two rules. First, do not fight existing workflows. Google Sheets stays because staff already knows it. Second, databases are for truth, not data entry. PostgreSQL exists to answer questions like "who needs a reminder tomorrow?" Everything else flows from this.
Understanding the Clinic Schema Through SQL
Instead of diagrams or abstractions, I'll explain the schema directly through SQL, because that's how most engineers reason about systems in practice.
Leads
Leads are intentionally lightweight. They represent interest, not commitment.
CREATE TABLE leads (
lead_id VARCHAR(20) PRIMARY KEY,
lead_date DATE NOT NULL,
name VARCHAR(150),
phone_number VARCHAR(20) NOT NULL,
address TEXT,
source_lead VARCHAR(50),
notes TEXT,
lead_follow_up_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
This table exists to answer a very narrow question: who contacted the clinic and when should someone follow up next? Nothing downstream depends on this table directly, and that's by design. No business logic lives here. No reminders depend on it.
Patients
Patients are the anchor of the entire system. This is the master table.
CREATE TABLE patients (
patient_id VARCHAR(20) PRIMARY KEY,
name VARCHAR(150) NOT NULL,
phone_number VARCHAR(20) UNIQUE NOT NULL,
age INT,
gender VARCHAR(20),
email VARCHAR(150),
address TEXT,
birthday_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
The most important decision here is treating phone_number as a first-class identifier. It's unique, it matches the real world, and it simplifies integrations with WhatsApp, billing, and reminders. IDs are for databases; phone numbers are how clinics actually operate.
Visits
Visits capture what already happened. They are descriptive, not operational.
CREATE TABLE visits (
visit_id VARCHAR(20) PRIMARY KEY,
patient_id VARCHAR(20) REFERENCES patients(patient_id),
visit_date DATE NOT NULL,
doctor_name VARCHAR(150),
diagnosis TEXT,
next_follow_up_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Visits record what happened, not what will happen. While visits may include a suggested follow-up date, they are not used to trigger reminders. That responsibility lives elsewhere.
Treatments
Treatments are where automation starts to matter. This is the most important table in the system.
CREATE TABLE treatments (
treatment_id VARCHAR(20) PRIMARY KEY,
patient_id VARCHAR(20) REFERENCES patients(patient_id),
treatment_name VARCHAR(150) NOT NULL,
start_date DATE,
next_due_date DATE,
current_status VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT treatments_current_status_check
CHECK (UPPER(current_status) IN ('PLANNED','ONGOING','COMPLETED','PAUSED','CANCELLED'))
);
A treatment represents ongoing intent: laser sessions, skin procedures, multi-sitting therapies. If a treatment has a next_due_date and its status is active, the system knows a reminder needs to go out. There is no scheduling logic outside the database. The data itself defines behavior. No calendar tables. No cron math. Just data.
Billing
Billing is connected to reality but intentionally isolated from scheduling logic.
CREATE TABLE bills (
bill_id BIGSERIAL PRIMARY KEY,
phone_number VARCHAR(20) REFERENCES patients(phone_number),
bill_date DATE NOT NULL,
total_amount NUMERIC(10,2),
amount_paid NUMERIC(10,2),
payment_mode VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Billing exists for financial clarity, not automation triggers. This separation keeps reminder logic clean and predictable. It matters for operations, not scheduling.
Workflow One: Automated Daily Reminders
Every day at 9:00 AM UTC, an n8n workflow runs automatically. It calculates tomorrow's date and asks the database a single, well-defined question.
SELECT
p.name,
p.phone_number,
t.treatment_name,
t.next_due_date
FROM treatments t
JOIN patients p ON p.patient_id = t.patient_id
WHERE t.next_due_date = CURRENT_DATE + INTERVAL '1 day'
AND t.current_status IN ('Planned', 'Ongoing');
That's the reminder engine. The logic is embarrassingly simple—because it should be. The result set is converted into JSON and sent to a WhatsApp API endpoint using whatsapp-web.js. There are no edge-case branches or retries embedded in business logic. SQL already filtered the truth. n8n simply moves it forward.
Workflow Two: Google Sheets to PostgreSQL Sync
The second workflow exists to keep humans out of the database. The staff should never touch the database.
Every night, n8n pulls all rows from a configured Google Sheet, truncates the target table in PostgreSQL, and inserts a fresh copy. This full-refresh approach avoids merge complexity, partial updates, merge conflicts, and silent inconsistencies. The database always mirrors the spreadsheet exactly. The staff continues working in Sheets. PostgreSQL remains clean, predictable, and automation-friendly.
Why This Reduced Friction Immediately
Once this went live, the change was immediate. No one had to remember to send reminders. No one checked sheets every evening. No one debated ownership of follow-ups. The staff focused on patient conversations, treatment explanations, and closing sales. The system handled dates, comparisons, and notifications quietly in the background. That's the correct division of labor.
Closing Thoughts
There are many other problems to solve in a clinic—inventory, marketing attribution, retention—but this was the highest-leverage fix. Not because it was complex. But because it removed invisible friction. Not by adding software, but by assigning responsibility correctly.
As engineers, our job isn't to add dashboards. It's to make sure the system quietly does its job every single day. And at 9 AM, without fail, this one does.
If this kind of practical automation interests you—or if you've built something similar—I'd love to hear about it. Feel free to start a conversation in the comments or drop me an email at diljit@diljitpr.net.
~ Comments & Discussion ~
Have thoughts on this post? Join the discussion below! Comments are powered by Disqus.