All articles
Build Guides

Build a WhatsApp Support Agent With Your Docs Using n8n, Supabase & Gemini

FDE Coach EditorialAugust 31, 202612 min read

What We’re Building

A WhatsApp chatbot that answers customer questions using your documentation, not a generic LLM hallucination. When a user sends a message, the system retrieves the most relevant chunks from your docs stored in Supabase’s pgvector, feeds them as context to Google Gemini, and replies via the WhatsApp Business Cloud API—all orchestrated by a self-hosted n8n workflow.

Feature list you ship:

  • Real-time WhatsApp message ingestion via webhook
  • Semantic search across your own knowledge base (PDFs, markdown, HTML)
  • Context-augmented answer generation using Gemini 1.5 Flash (free tier)
  • Conversation threading with message context
  • Fully serverless-optional architecture with zero recurring cost at low volume
  • Audit trail of every Q&A pair in Supabase

Architecture: How the Pieces Fit

Before touching a terminal, internalize the flow. A message hits the WhatsApp webhook, n8n picks it up, fetches the last few messages for context, queries the vector store for semantically similar document chunks, constructs a prompt with those chunks, calls Gemini, and returns the answer.

The retrieval-augmented generation (RAG) pattern here is the same one powering production support agents at scale. If you want a deeper dive into why bounded context matters when chaining AI steps, read Domain-Driven Agents: Bounded Contexts for Reliable AI Workflows.

Prerequisites (All Free Tier)

Grab these before you start. Every link points to a free tier or open-source repo.

ResourcePurposeFree Tier Limit
n8n self-hostedWorkflow orchestrationUnlimited workflows, self-hosted
SupabasePostgreSQL + pgvector2 projects, 500 MB DB, 2 GB bandwidth
Google AI StudioGemini API key15 RPM, 1M tokens/min (Gemini 1.5 Flash)
Meta WhatsApp Business APISend/receive messages1,000 conversations/month free
ngrok (or Cloudflare Tunnel)Expose local n8n to WhatsApp webhook1 static domain free

A local machine or a $6/month VPS is enough. n8n runs on Node.js 18+.

Step 1: Provision Supabase & Enable pgvector

Create a project at supabase.com, then enable the vector extension in the SQL editor:

-- Enable pgvector extension
create extension if not exists vector with schema extensions;

-- Create the docs table
create table if not exists docs (
  id bigint primary key generated always as identity,
  content text not null,
  metadata jsonb default '{}'::jsonb,
  embedding vector(768)
);

-- Create an index for similarity search
create index on docs using ivfflat (embedding vector_cosine_ops) with (lists = 100);

-- Create a conversation log table
create table if not exists conversations (
  id bigint primary key generated always as identity,
  phone_number text not null,
  role text check (role in ('user', 'assistant')) not null,
  message text not null,
  created_at timestamptz default now()
);

create index on conversations (phone_number, created_at desc);

The embedding column uses 768 dimensions because Gemini’s text-embedding-004 model outputs 768-dimensional vectors. The conversations table gives us threading context later.

Step 2: Ingest Your Docs Into the Vector Store

You need a script that chunks your docs, generates embeddings via Gemini’s embedding endpoint, and upserts into Supabase. The free tier gives you 1,500 embedding requests per minute—plenty for a one-time ingestion.

Create a Node.js script (ingest.mjs):

import { createClient } from '@supabase/supabase-js';
import { GoogleGenerativeAI } from '@google/generative-ai';
import fs from 'fs/promises';
import path from 'path';

const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY;
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;

const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);

async function chunkText(text, maxChars = 1000) {
  const paragraphs = text.split(/\n\n+/);
  const chunks = [];
  let current = '';
  for (const p of paragraphs) {
    if ((current + p).length > maxChars && current) {
      chunks.push(current.trim());
      current = p;
    } else {
      current += (current ? '\n\n' : '') + p;
    }
  }
  if (current.trim()) chunks.push(current.trim());
  return chunks;
}

async function getEmbedding(text) {
  const model = genAI.getGenerativeModel({ model: 'text-embedding-004' });
  const result = await model.embedContent(text);
  return result.embedding.values;
}

async function main() {
  const docsDir = './docs';
  const files = await fs.readdir(docsDir);
  
  for (const file of files) {
    if (!file.endsWith('.md') && !file.endsWith('.txt')) continue;
    const content = await fs.readFile(path.join(docsDir, file), 'utf-8');
    const chunks = await chunkText(content);
    
    for (const chunk of chunks) {
      const embedding = await getEmbedding(chunk);
      const { error } = await supabase.from('docs').insert({
        content: chunk,
        metadata: { source: file },
        embedding
      });
      if (error) console.error('Insert error:', error);
      else console.log(`Ingested chunk from ${file}`);
    }
  }
}

main();

Run it:

npm install @supabase/supabase-js @google/generative-ai
SUPABASE_URL=https://xxx.supabase.co \
SUPABASE_SERVICE_KEY=eyJ... \
GEMINI_API_KEY=AIza... \
node ingest.mjs

Dump your markdown files, PDF-to-text exports, or HTML-to-text into ./docs and let it rip.

Step 3: Configure the Gemini API

Head to Google AI Studio, click "Get API Key," and create one for a new project. The free tier for Gemini 1.5 Flash gives you 15 requests per minute and 1 million tokens per minute—comfortable for a support agent handling dozens of concurrent users.

Test your key:

curl -s "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=$GEMINI_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"contents":[{"parts":[{"text":"Say hello in exactly 5 words."}]}]}'

Store the key in n8n as a credential (Settings → Credentials → Google Gemini API).

Step 4: Set Up the WhatsApp Business API

  1. Go to Meta Developer Portal → Create App → "Business" type.
  2. Add the "WhatsApp" product, select "Business" as the use case.
  3. Choose a test phone number (Meta provides a free one for development).
  4. Under "Configuration," set the webhook URL to https://your-ngrok-domain.ngrok-free.app/webhook/whatsapp.
  5. Set the verify token to a random string you generate (e.g., openssl rand -hex 16).
  6. Subscribe to the messages webhook field.

The webhook sends a JSON payload containing the user’s phone number and message text inside entry[0].changes[0].value.messages[0]. The verify token challenge happens at webhook registration—n8n handles it cleanly.

Step 5: Build the n8n Workflow

Self-host n8n first:

npm install n8n -g
export N8N_PORT=5678
n8n start

Open http://localhost:5678, then build the workflow node by node.

Node 1: Webhook Trigger

  • Add a Webhook node, set HTTP Method to POST, Path to /whatsapp.
  • Enable "Respond to Webhook" with a static 200 response so WhatsApp doesn’t retry.
  • Under Options, add a Response Code of 200 and a Response Body of "ok".

Node 2: Verify Token Challenge

WhatsApp sends a hub.challenge GET parameter during registration. Add a Switch node after the webhook that checks {{ $json.query.hub_mode }}:

  • If it equals "subscribe", return {{ $json.query.hub_challenge }} directly as the webhook response.
  • Otherwise, proceed to the message handler.

Node 3: Extract Message Payload

Add a Set node to extract what you need:

// In the "Keep Only Set" mode, set these values:
phone = {{ $json.entry[0].changes[0].value.messages[0].from }}
message = {{ $json.entry[0].changes[0].value.messages[0].text.body }}
wa_business_id = {{ $json.entry[0].changes[0].value.metadata.phone_number_id }}

Node 4: Fetch Conversation History

Add a Supabase node (install the community node n8n-nodes-supabase or use the HTTP Request node). Query the last 6 messages:

SELECT role, message FROM conversations
WHERE phone_number = '{{ $json.phone }}'
ORDER BY created_at DESC
LIMIT 6

Reverse the array in a Set node so the oldest message comes first.

Add an HTTP Request node targeting Supabase’s RPC or REST API. First, generate the query embedding using Gemini’s embedding endpoint, then use Supabase’s rpc function. Create a stored procedure in Supabase’s SQL editor:

create or replace function match_docs (
  query_embedding vector(768),
  match_count int default 3
) returns table (
  id bigint,
  content text,
  similarity float
) language plpgsql as $$
begin
  return query
  select
    docs.id,
    docs.content,
    1 - (docs.embedding <=> query_embedding) as similarity
  from docs
  order by docs.embedding <=> query_embedding
  limit match_count;
end;
$$;

In n8n, first call Gemini’s embedding endpoint via HTTP Request:

Method: POST
URL: https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key={{ $credentials.geminiApi.apiKey }}
Body: { "content": { "parts": [{ "text": "{{ $json.message }}" }] } }

Then call Supabase RPC with the resulting embedding vector.

Node 6: Generate Answer with Gemini

Add another HTTP Request node (or the native Google Gemini node if you installed it). Construct a prompt:

Method: POST
URL: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={{ $credentials.geminiApi.apiKey }}
Body:
{
  "systemInstruction": {
    "parts": [{ "text": "You are a helpful customer support agent. Answer the user's question using ONLY the provided documentation below. If the docs don't contain the answer, say you don't know and offer to escalate. Never make up information." }]
  },
  "contents": [{
    "role": "user",
    "parts": [{
      "text": "Documentation:\n{{ $json.docsChunks }}\n\nConversation history:\n{{ $json.history }}\n\nUser's latest question: {{ $json.message }}"
    }]
  }]
}

Node 7: Send WhatsApp Reply

Add an HTTP Request node to call the WhatsApp Cloud API:

Method: POST
URL: https://graph.facebook.com/v18.0/{{ $json.wa_business_id }}/messages
Headers:
  Authorization: Bearer {{ $credentials.whatsappApi.accessToken }}
Body:
{
  "messaging_product": "whatsapp",
  "to": "{{ $json.phone }}",
  "text": { "body": "{{ $json.generatedAnswer }}" }
}

Node 8: Log to Conversations

Add a final Supabase (or HTTP Request) node that inserts two rows: the user’s message and the assistant’s reply.

INSERT INTO conversations (phone_number, role, message) VALUES
('{{ $json.phone }}', 'user', '{{ $json.message }}'),
('{{ $json.phone }}', 'assistant', '{{ $json.generatedAnswer }}')

How to Run and Test It

  1. Start ngrok: ngrok http 5678
  2. Copy the ngrok URL into your WhatsApp app’s webhook configuration.
  3. Send a message from your test WhatsApp number to the business number.
  4. Watch the n8n execution log. The webhook fires, the vector search runs, Gemini responds, and WhatsApp delivers the answer.

If you’re debugging, open the n8n execution detail—every node shows its input/output JSON. This is the same structured debugging approach we teach for production AI rollouts; see Reading the Tea Leaves: Customer Health Signals an FDE Monitors During an AI Rollout for the mindset.

Extensions That Pay Off Immediately

Add a human handoff trigger. If Gemini’s response contains a keyword like "escalate" or if the similarity score of the top chunk is below 0.7, forward the message to a Slack channel with the conversation context. One additional HTTP Request node to Slack’s incoming webhook.

Multilingual support. Gemini 1.5 Flash handles 100+ languages natively. Add a detection step: if the user’s message language differs from your docs’ language, prepend "Translate your answer to the user’s language" to the system instruction.

Scheduled doc re-ingestion. Add an n8n Cron trigger that runs your ingest script weekly, picking up updated docs. Combine it with the Build a Cold Outreach Email Personalizer From a CSV of Prospects Using OpenRouter Free Models pattern for scheduled batch jobs.

Rate limiting with a simple counter. Add a Supabase query that counts messages from this phone number in the last minute. If it exceeds a threshold, reply with a polite "please wait" message. This prevents Gemini free-tier quota exhaustion.

Common Pitfalls and How to Avoid Them

Webhook timeout. WhatsApp expects a 200 response within 20 seconds. If your Gemini call takes longer, WhatsApp retries and the user gets duplicate answers. Fix: respond to the webhook immediately, then process asynchronously using n8n’s "Respond to Webhook" node with a static response, letting the workflow continue in the background.

Vector dimension mismatch. If you accidentally use a model that outputs 1536-dimensional embeddings (like OpenAI’s) against a vector(768) column, the insert fails silently in some clients. Double-check the dimension in your Supabase table definition matches text-embedding-004.

Empty chunks from bad PDF parsing. A PDF-to-text library can produce pages of whitespace. Filter chunks with fewer than 50 meaningful characters before ingesting.

Gemini free-tier rate limit. At 15 RPM, concurrent users can hit the ceiling. Implement a queue or use n8n’s built-in "Error Trigger" node to retry with exponential backoff.

WhatsApp template approval. The Cloud API’s test number can send free-form replies to users who message first, but if you initiate a conversation outside the 24-hour window, you need an approved message template. Stay within the customer service window.

FAQ

Can I use a different vector database? Yes. Replace Supabase with Pinecone’s free tier, Weaviate Cloud, or even a local ChromaDB instance. The n8n HTTP Request node pattern stays identical; only the API endpoint changes.

What if my docs are 10,000+ pages? The ivfflat index scales to millions of vectors. For very large datasets, increase the lists parameter to sqrt(row_count) and consider hybrid search (keyword + vector) using Supabase’s full-text search alongside pgvector.

Does this work with WhatsApp groups? The webhook payload includes a group_id when the bot is added to a group. You’d extract that instead of the individual phone number and adjust the conversation threading accordingly.

How do I deploy this permanently? Self-host n8n on a $6/month Hetzner VPS or use Railway’s free tier. Replace ngrok with a Cloudflare Tunnel (free, permanent domain) or set up a reverse proxy with Caddy and Let’s Encrypt.

How do I improve answer quality? The single biggest lever is chunk quality. Overlapping chunks, metadata filtering (e.g., only search docs tagged "billing" for billing questions), and a re-ranking step all help. For a deeper architecture discussion, see Domain-Driven Agents: Bounded Contexts for Reliable AI Workflows.

My Gemini responses are too slow. Gemini 1.5 Flash typically responds in under 2 seconds. If you’re seeing latency, check your n8n instance’s geographic proximity to Google’s API endpoints. Deploying n8n in us-central1 or europe-west4 reduces round-trip time.

#customer-support#rag#n8n#chatbot

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now
Build a WhatsApp Support Agent With Your Docs Using n8n, Supabase & Gemini | FDE Coach