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.
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
/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.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.
| Service | What it's for | Link |
|---|---|---|
| Meta for Developers | WhatsApp Business API — sending and receiving messages | developers.facebook.com |
| Supabase | PostgreSQL database — stores all your reminders, contacts, events | supabase.com |
| Google AI Studio | Gemini API key (Tier 1 & 2 AI) | aistudio.google.com |
| Groq | Llama 3 API key (Tier 3 AI fallback) | console.groq.com |
| OpenRouter | GPT-4o-mini (Tier 4 AI fallback — minimal paid usage) | openrouter.ai |
| Tavily | Web search (primary) | tavily.com |
| Serper | Web search fallback | serper.dev |
| Render | Hosting — free web service tier | render.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.
| Variable | Description | |
|---|---|---|
PORT | Port the Express server listens on. Render sets this automatically. | |
VERIFY_TOKEN | Any string you choose. Must match what you set in the Meta webhook config. | required |
MY_PHONE_NUMBER | Your WhatsApp number in international format, no +. E.g. 919876543210 | required |
PHONE_NUMBER_ID | From Meta developer console → WhatsApp → API Setup. | required |
ACCESS_TOKEN | Meta permanent access token. Generate one from the Meta developer portal. | required |
SUPABASE_URL | Your Supabase project URL. Found in Project Settings → API. | required |
SUPABASE_KEY | Supabase anon public key. Found in Project Settings → API. | required |
GEMINI_API_KEY | From Google AI Studio. Used for Tier 1 & 2 AI. | required |
GROQ_API_KEY | From Groq console. Used for Tier 3 AI fallback. | required |
OPENROUTER_API_KEY | From OpenRouter. Used for Tier 4 AI fallback. | required |
TAVILY_API_KEY | From Tavily. Primary web search provider. | required |
SERPER_API_KEY | From Serper.dev. Web search fallback. | required |
PUBLIC_URL | Your application's public URL (e.g. https://manvi.onrender.com). Required for self-ping keep-alive — do not include a trailing slash. | required |
CRON_SECRET | Any random string. Protects /api/tick from unauthorized calls. Must match what you set in cron-job.org. | required |
WEBHOOK_APP_SECRET | Your 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.
- Set Webhook URL to
https://your-app.onrender.com/webhook - Set Verify token to the same value as your
VERIFY_TOKENenv var - Subscribe to the messages webhook field
- Add your phone number as a test recipient in the API Setup section
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
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.
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.
Web search
Manvi searches the web and returns a clean plain-text summary. No links, no markdown — just the answer.
Who won the IPL last season?
What's the weather in Hyderabad today?
Latest news about OpenAI
Primary provider is Tavily (1,000 req/month free). If Tavily is exhausted, Serper is used automatically (2,500 req lifetime free). You are alerted at 50, 10, and 0 remaining on each.
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
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
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
- Push your repo (with a filled
.env.examplebut no.env) to GitHub - In Render, create a new Web Service → connect your GitHub repo
- Set Build command:
npm install - Set Start command:
npm start - Add all environment variables in the Render Environment tab
- Deploy. Your URL will be
https://your-app.onrender.com
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.
- Create a free account at cron-job.org
- Add a new cronjob — method: GET
- URL:
https://your-app.onrender.com/api/tick?secret=YOUR_CRON_SECRET - 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.
- Add a New Monitor of type HTTP(s)
- URL:
https://your-app.onrender.com/api/ping - 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.
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.
| Tier | Model | Daily limit | Cost |
|---|---|---|---|
| 1 | Gemini 3 Flash Preview | ~20 req/day (shared) | Free |
| 2 | Gemini 2.5 Flash | ~20 req/day (shared with Tier 1) | Free |
| 3 | Groq Llama 3.3 70B | 300 req/day (safety cap) | Free |
| 4 | OpenRouter GPT-4o-mini | 50 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
| Method | Path | Description |
|---|---|---|
GET | / | Landing page |
GET | /documentation | This documentation page |
GET | /status | Manvi — live system dashboard |
GET | /api/ping | Health 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/status | Dashboard data — usage stats, uptime, limits, jobs, version |
GET | /webhook | Meta webhook verification handshake |
POST | /webhook | Incoming WhatsApp messages — signature-verified, rate-limited, main message handler |
Usage limits
| Service | Limit | Reset |
|---|---|---|
| Gemini (Tier 1 + 2 combined) | 40 req/day | Midnight IST |
| Groq | 300 req/day (safety cap) | Midnight UTC |
| OpenRouter | 50 req/day (safety cap) | Midnight UTC |
| Tavily | 1,000 req/month | Monthly |
| Serper | 2,500 req lifetime | Never 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.