Documentation
Home Status GitHub

Introduction

Manvi is a personal AI assistant that operates entirely inside WhatsApp. Send a message in plain English — she understands your intent, executes the action, and responds instantly. No app to install, no account to create beyond your existing WhatsApp.

Manvi is self-hosted. You run your own instance on Render, connect it to your own Meta WhatsApp Business account, and the only data that touches a third-party service is your API calls to Gemini, Groq, and your Supabase database.

Who this is for

Manvi is designed for a single owner — you. It is not a multi-tenant service. All personal data (reminders, contacts, events) is private to the WhatsApp number you configure as MY_PHONE_NUMBER.

What Manvi can do in v1.1.1

⏰ Reminders
One-off reminders at a specific time or on a future date. Vague times like "morning" or "evening" are resolved automatically.
🔁 Daily Routines
Recurring tasks at a fixed time, every day indefinitely.
📅 Weekly & Monthly Recurring
Reminders on a specific day of the week or day of the month — every Tuesday, the 1st of every month, etc.
✏️ Edit & Undo
Correct the last reminder with a follow-up — "Actually make that 6 PM" updates it in place.
🧠 Conversational Memory
Manvi remembers the last 4 messages, enabling natural follow-up questions without repeating context.
🎂 Birthdays & Events
Save once, get reminded every year with advance alerts.
📇 Contacts & Messaging
Save contacts and forward messages by name.
🔍 Web Search
Real-time web search via Tavily with Serper fallback. Results formatted for WhatsApp.
💬 Conversational AI
4-tier AI waterfall for general questions and chat.
🗑 Delete Tasks
Delete any reminder, routine, recurring task, or event by name.
🎤 Media Handling
Voice notes, images, and other media get a clear "text only" reply instead of silently failing.
🩺 Health Tracking
Visual 90-day uptime history with downtime detection (Red/Green) and "Last Run" tracking for all background jobs.
⚡️ Keep-Alive
External cron trigger (/api/tick) called every minute by cron-job.org ensures reminders fire even after a cold restart. Self-ping every 4 minutes as secondary keep-alive.
🔒 Webhook Security
Verifies Meta's X-Hub-Signature-256 on every incoming webhook. Per-user rate limiting (10 messages/minute) protects AI quota from abuse.

Prerequisites

Before you start, create accounts on these services. All free tiers are sufficient.

ServiceWhat it's forLink
Meta for DevelopersWhatsApp Business API — sending and receiving messagesdevelopers.facebook.com
SupabasePostgreSQL database — stores all your reminders, contacts, eventssupabase.com
Google AI StudioGemini API key (Tier 1 & 2 AI)aistudio.google.com
GroqLlama 3 API key (Tier 3 AI fallback)console.groq.com
OpenRouterGPT-4o-mini (Tier 4 AI fallback — minimal paid usage)openrouter.ai
TavilyWeb search (primary)tavily.com
SerperWeb search fallbackserper.dev
RenderHosting — free web service tierrender.com

Quick start

1. Fork and clone the repo

git clone https://github.com/viswabnath/whatsapp-reminder-bot
cd manvi-whatsapp-assistant
npm install

2. Copy the environment file

cp .env.example .env

Open .env and fill in all required keys. See Environment variables for the full list.

3. Set up the database

In your Supabase project, open the SQL Editor and run the full schema. See Database setup.

4. Run locally

npm run dev

The server starts on http://localhost:3000. Your dashboard is at http://localhost:3000/status.

5. Run the test suite

node test.js

This validates all v1.1 features against your real Supabase instance and cleans up after itself.

Environment variables

All secrets live in a .env file at the project root. Never commit this file.

VariableDescription
PORTPort the Express server listens on. Render sets this automatically.
VERIFY_TOKENAny string you choose. Must match what you set in the Meta webhook config.required
MY_PHONE_NUMBERYour WhatsApp number in international format, no +. E.g. 919876543210required
PHONE_NUMBER_IDFrom Meta developer console → WhatsApp → API Setup.required
ACCESS_TOKENMeta permanent access token. Generate one from the Meta developer portal.required
SUPABASE_URLYour Supabase project URL. Found in Project Settings → API.required
SUPABASE_KEYSupabase anon public key. Found in Project Settings → API.required
GEMINI_API_KEYFrom Google AI Studio. Used for Tier 1 & 2 AI.required
GROQ_API_KEYFrom Groq console. Used for Tier 3 AI fallback.required
OPENROUTER_API_KEYFrom OpenRouter. Used for Tier 4 AI fallback.required
TAVILY_API_KEYFrom Tavily. Primary web search provider.required
SERPER_API_KEYFrom Serper.dev. Web search fallback.required
PUBLIC_URLYour application's public URL (e.g. https://manvi.onrender.com). Required for self-ping keep-alive — do not include a trailing slash.required
CRON_SECRETAny random string. Protects /api/tick from unauthorized calls. Must match what you set in cron-job.org.required
WEBHOOK_APP_SECRETYour Meta App Secret — Meta Developer Console → App → Settings → Basic → App Secret. Enables X-Hub-Signature-256 verification on every incoming webhook.recommended

Database setup

Run this SQL in the Supabase SQL Editor once. It creates all 8 tables from scratch.

SQL-- Contacts address book
CREATE TABLE contacts (
  id    SERIAL PRIMARY KEY,
  name  VARCHAR(50) NOT NULL UNIQUE,
  phone VARCHAR(20) NOT NULL
);

-- One-off reminders
CREATE TABLE personal_reminders (
  id            SERIAL PRIMARY KEY,
  phone         VARCHAR(20) NOT NULL,
  message       TEXT NOT NULL,
  reminder_time TIMESTAMP WITH TIME ZONE NOT NULL,
  group_name    VARCHAR(50),
  status        VARCHAR(20) DEFAULT 'pending',
  created_at    TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Daily routines (time only, fires every day)
CREATE TABLE daily_routines (
  id              SERIAL PRIMARY KEY,
  phone           VARCHAR(20) NOT NULL,
  task_name       TEXT NOT NULL,
  reminder_time   TIME NOT NULL,
  is_active       BOOLEAN DEFAULT TRUE,
  last_fired_date DATE
);

-- Weekly and monthly recurring tasks
CREATE TABLE recurring_tasks (
  id               BIGSERIAL PRIMARY KEY,
  phone            VARCHAR(20) NOT NULL,
  task_name        TEXT NOT NULL,
  reminder_time    TIME NOT NULL,
  recurrence_type  TEXT NOT NULL CHECK (recurrence_type IN ('weekly', 'monthly')),
  day_of_week      INTEGER,
  day_of_month     INTEGER,
  is_active        BOOLEAN DEFAULT TRUE,
  last_fired_date  DATE,
  created_at       TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Birthdays and special events
CREATE TABLE special_events (
  id          SERIAL PRIMARY KEY,
  phone       VARCHAR(20) NOT NULL,
  event_type  VARCHAR(50),
  person_name VARCHAR(100),
  event_date  DATE NOT NULL,
  created_at  TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- All incoming messages and bot responses (also powers conversational memory)
CREATE TABLE interaction_logs (
  id           SERIAL PRIMARY KEY,
  sender_name  VARCHAR(50),
  sender_phone VARCHAR(20) NOT NULL,
  message      TEXT NOT NULL,
  bot_response TEXT,
  created_at   TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Daily API usage tracking (stores uptime history)
CREATE TABLE api_usage (
  usage_date       DATE PRIMARY KEY,
  gemini_count     INT DEFAULT 0,
  groq_count       INT DEFAULT 0,
  openrouter_count INT DEFAULT 0,
  tavily_count     INT DEFAULT 0,
  serper_count     INT DEFAULT 0,
  error_count      INT DEFAULT 0
);

-- v1.1.1: Background job health tracking
CREATE TABLE system_jobs (
    job_name TEXT PRIMARY KEY,
    last_fired TIMESTAMPTZ,
    status TEXT DEFAULT 'active'
);

INSERT INTO system_jobs (job_name, status)
VALUES
    ('Reminder Dispatch', 'active'),
    ('Routine Dispatch', 'active'),
    ('Recurring Task Dispatch', 'active'),
    ('Event Alert', 'active')
ON CONFLICT (job_name) DO NOTHING;

-- Atomic counter increment for api_usage (run once, required for usage tracking)
CREATE OR REPLACE FUNCTION increment_api_usage(p_date DATE, p_column TEXT)
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
  col_name TEXT := p_column || '_count';
BEGIN
  EXECUTE format(
    'UPDATE api_usage SET %I = COALESCE(%I, 0) + 1 WHERE usage_date = $1',
    col_name, col_name
  ) USING p_date;
END;
$$;

Meta webhook setup

In the Meta developer portal, go to your app → WhatsApp → Configuration.

  1. Set Webhook URL to https://your-app.onrender.com/webhook
  2. Set Verify token to the same value as your VERIFY_TOKEN env var
  3. Subscribe to the messages webhook field
  4. Add your phone number as a test recipient in the API Setup section
Permanent access token

The default access token from Meta expires in 24 hours. Generate a permanent token via the System User flow in Meta Business Manager. Use that token as ACCESS_TOKEN.

Reminders

Set a one-off reminder in plain English. Manvi extracts the time and optional date, saves it with a full timezone-aware timestamp, and fires it via WhatsApp at the right moment.

Example messages

Remind me to call the bank at 3 PM
Remind me in 20 minutes to check the oven
5th April is Ravi's interview, remind me at 9 AM on that day

How time is resolved

  • Relative times ("in 20 minutes") — the AI calculates the exact IST timestamp from the current time
  • Future dates with time ("5th April at 9 AM") — stored as a full TIMESTAMPTZ, fires once and is marked complete
  • Time only with no date ("at 3 PM") — defaults to today, rolls to tomorrow if the time has already passed
Timezone

All times are IST (Asia/Kolkata, UTC+5:30). The scheduler runs every minute and compares against the current IST timestamp.

Daily routines

Routines repeat at the same time every day with no end date. They run at a fixed clock time (e.g. 9:00 AM every day) until you delete them.

For repeating alerts spaced by minutes or hours, see Interval reminders.

Remind me to drink water every day at 10 AM
Set a daily reminder to check email at 9 AM

To stop a routine, delete it by name: "Delete drink water routine"

Interval reminders

Repeat an alert every X minutes or hours within a set window. Useful for hydration reminders, medication, stretch breaks, or anything that needs nudging throughout the day.

Example messages

Remind me every 30 minutes to drink water
Every 1 hour remind me to stretch for the next 4 hours
Remind me every 15 mins to check on the oven for 2 hours

How it works

  • intervalMinutes — how often the alert fires. Minimum 5 minutes.
  • durationHours — how long to keep repeating. Defaults to 8 hours if not specified.
  • Manvi pre-schedules all alerts as individual rows in personal_reminders, so the existing scheduler picks them up automatically — no new cron job needed.

Viewing them

What are my reminders?

Interval reminders appear in a separate group showing count remaining and next fire time per task.

Cancelling them

Delete the drink water reminder
Stop drink water intervals

All pending alerts for that task are deleted in one shot — you do not need to delete them one by one.

Different from routines

Routines repeat at the same fixed time every day forever. Interval reminders repeat within a window (default 8 hours) and stop. Use routines for habits, interval reminders for tasks you want nudged throughout the day.

Birthdays & events

Special events are saved with a date and repeat every year. The year in the date is irrelevant after the first save — Manvi only matches day and month.

22nd May is Manu's birthday
Save Simhadri's farewell on 3rd April

Manvi sends two alerts every year:

  • Day before at 8:30 AM IST
  • The day itself at 8:30 AM IST

Contacts & messaging

Save a contact once. After that, address them by name in any instruction.

# Save a contact
Save mom as 919876543210
Add Ravi to contacts, number 919988776655

# Send a message by name
Tell mom I'll be 10 minutes late
Message Ravi and say the meeting is at 7

Phone numbers must include the country code with no + or spaces. Manvi strips all non-digit characters automatically before saving.

Deleting tasks

Delete any reminder, routine, or event by describing it. You do not need to use the exact saved name.

Delete the reminder to call the bank
Delete drink water routine
Remove Manu's birthday

Manvi searches reminders first, then routines, then events. The first match found is deleted. If nothing matches, it says so.

Conversational chat

For anything that is not a task, Manvi responds directly using the AI waterfall. Ask questions, request explanations, or just talk.

What do you have saved for me?
Explain machine learning simply
Tell me a joke

Weekly & monthly recurring reminders

For reminders that repeat on a specific day of the week or a specific day of the month — use weekly or monthly recurring tasks. These are stored separately from daily routines and have their own scheduler.

Weekly

Remind me to take out the trash every Tuesday at 8 PM
Every Friday at 6 PM remind me to submit my timesheet

Monthly

Remind me to pay rent on the 1st of every month at 9 AM
On the 15th of every month remind me to check bank statement
End of month edge case

If you set a monthly task for the 31st and the current month has fewer days, it fires on the last day of that month instead of being skipped.

To stop a recurring task, delete it by name: "Delete the pay rent reminder"

Edit & undo

Made a mistake on a reminder time? Just send a correction as a follow-up message. Manvi uses conversation history to identify which reminder you mean and updates it in place — no delete and recreate needed.

# First set a reminder
Remind me to buy eggs at 5 PM

# Immediately correct it
Actually make that 6 PM
Change the buy eggs reminder to 7 PM
Only works on pending reminders

Edit only targets one-off reminders that haven't fired yet. It does not edit daily routines, recurring tasks, or events. Use delete + recreate for those.

Conversational memory

Manvi reads the last 4 messages in your conversation before processing each new one. This lets you ask natural follow-up questions without repeating context.

# Ask a question
Who won IPL 2024?

# Follow up without repeating the topic
Who was the winning captain?
Where was the final held?

Memory is scoped strictly to your own phone number — other users' history is never mixed in.

Media handling

Manvi only processes text messages. If you accidentally send a voice note, image, video, document, or sticker, she replies with a clear message instead of silently ignoring it.

# Sending a voice note gets back:
I can only read text messages right now. I cannot process voice notes. Please type your request.

Deploy to Render

  1. Push your repo (with a filled .env.example but no .env) to GitHub
  2. In Render, create a new Web Service → connect your GitHub repo
  3. Set Build command: npm install
  4. Set Start command: npm start
  5. Add all environment variables in the Render Environment tab
  6. Deploy. Your URL will be https://your-app.onrender.com
Free tier sleep

Render free instances sleep after 15 minutes of inactivity. Set up a keep-alive ping — see below.

Keep-alive & uptime monitoring

Render free instances sleep after 15 minutes of inactivity. When sleeping, the in-process cron jobs stop ticking. The solution is two layers: an external cron trigger that runs dispatch jobs on-demand, and UptimeRobot for downtime alerting.

Step 1 — External cron trigger (cron-job.org)

This is the primary reliability mechanism. cron-job.org (free) calls /api/tick every minute. This both keeps the service awake and runs the reminder/routine dispatch logic — so reminders fire even after a cold restart.

  1. Create a free account at cron-job.org
  2. Add a new cronjob — method: GET
  3. URL: https://your-app.onrender.com/api/tick?secret=YOUR_CRON_SECRET
  4. Schedule: every 1 minute

Set CRON_SECRET and PUBLIC_URL in your Render environment variables. The server also self-pings /api/tick every 4 minutes as a secondary keep-alive.

Step 2 — Downtime alerting (UptimeRobot)

Use UptimeRobot (free) for mobile push alerts if your bot goes down.

  1. Add a New Monitor of type HTTP(s)
  2. URL: https://your-app.onrender.com/api/ping
  3. Interval: 5 minutes

The /api/ping endpoint returns HTTP 200 when healthy and HTTP 500 if Supabase is unreachable. Keep this separate from /api/tick — it is a pure health check, not a job trigger.

Testing locally

The test suite runs against your real Supabase instance, inserts test data, verifies it, and cleans up after every run. No real WhatsApp messages are sent.

# Run the full suite
node test.js

# Run specific suites only
node test.js --suite formatter
node test.js --suite formatter,memory,recurring

Available suite keys: connectivity, builddate, ai, reminders, routines, events, contacts, delete, interval, scheduler, usage, routes, memory, missingtime, edit, formatter, vaguedefaults, recurring, media.

AI-dependent suites

Suites that call the AI waterfall (ai, memory, missingtime, edit, vaguedefaults, recurring) consume real quota and are non-deterministic. If quota is exhausted mid-run, remaining AI cases are skipped automatically. Run formatter, media, and connectivity for fast deterministic checks.

AI waterfall

Every message goes through a 4-tier AI chain. If a tier is exhausted or fails, the next tier is tried automatically.

TierModelDaily limitCost
1Gemini 3 Flash Preview~20 req/day (shared)Free
2Gemini 2.5 Flash~20 req/day (shared with Tier 1)Free
3Groq Llama 3.3 70B300 req/day (safety cap)Free
4OpenRouter GPT-4o-mini50 req/day (safety cap)~$5 credit

The model name and remaining quota is appended to every bot response so you always know which tier was used. Conversation history (last 4 turns) is injected into the system prompt on every call to support follow-up questions.

API endpoints

MethodPathDescription
GET/Landing page
GET/documentationThis documentation page
GET/statusManvi — live system dashboard
GET/api/pingHealth check for UptimeRobot. Returns {status, latency_ms, timestamp}. HTTP 500 if Supabase unreachable.
GET/api/tick?secret=…External cron trigger — runs reminder, routine, and recurring task dispatch. Protected by CRON_SECRET. Called by cron-job.org every minute.
GET/api/statusDashboard data — usage stats, uptime, limits, jobs, version
GET/webhookMeta webhook verification handshake
POST/webhookIncoming WhatsApp messages — signature-verified, rate-limited, main message handler

Usage limits

ServiceLimitReset
Gemini (Tier 1 + 2 combined)40 req/dayMidnight IST
Groq300 req/day (safety cap)Midnight UTC
OpenRouter50 req/day (safety cap)Midnight UTC
Tavily1,000 req/monthMonthly
Serper2,500 req lifetimeNever resets

Manvi alerts you on WhatsApp when any search service hits 50, 10, or 0 remaining credits. Live usage is visible on the status dashboard.

Troubleshooting

Server won't start — "supabaseUrl is required"

You are running the server from inside the src/ directory. dotenv looks for .env relative to the working directory, not the file. Always run from the project root:

npm run dev       # correct
cd src && node server.js   # wrong — .env not found

Cannot find module 'server.js'

Your package.json scripts point to src/server.js. Don't run node server.js from the root — use npm start or npm run dev.

"Contact not found in address book"

The contact does not exist in the contacts table. Save them first: "Save mom as 919876543210"

Reminders firing on wrong date

Make sure you specify both a date and a time. If you only specify a time, Manvi defaults to today (rolling to tomorrow if passed). For future dates, say the date explicitly.

Dashboard shows no data

The api_usage table populates as you use the bot. In v1.1.1+, a record is auto-created every day the bot is online (even if idle) to ensure your 90-day history grid remains continuous.