How I Built an AI Portfolio Assistant with Gemini and Supabase
Fyra, the assistant on this site, is a Next.js chat route that finds the most relevant parts of my portfolio for each question, hands only those to Google's Gemini API with strict instructions, and opens a lead form when a visitor is ready to describe a project. The retrieval is plain keyword scoring over projects and services stored in Supabase, not a vector database, and it's enough for a portfolio of this size. This note walks through how it works and why I made each choice. The project page is Fyra.
What it has to do
A portfolio has an odd job. Visitors arrive with a specific need ("can you build a voice assistant for a clinic?") and have to work out from static pages whether I've done anything close. Fyra answers that question directly:
- match the visitor's idea to real projects I've built,
- explain what would decide the scope, without inventing prices or timelines,
- and when they're ready, collect a structured brief instead of a loose chat.
The request flow
Every message goes through one API route:
POST /api/fyra/chat
1. per-IP rate limit
2. validate the body with Zod (message up to 500 characters)
3. hash the IP with SHA-256; find or create the chat session
4. retrieve: score projects and services against the message
5. add the page the visitor has open and their approximate country
6. build the system prompt; call Gemini
7. save messages only if the conversation qualifies
8. return the reply plus cards for the projects it mentions
Retrieval without embeddings
The retrieval step is about a hundred lines of TypeScript. It lowercases the message, drops stop words (including generic ones like "app", "website" and "build" that would match everything), and scores every project:
| Where the word matches | Points |
|---|---|
| Exactly the project name | 15 |
| Part of the project name | 8 |
| The tagline | 5 |
| The tech stack | 4 |
| The problem, solution or approach text | 2 each |
Services get a similar, simpler score. The top three projects and top two services go into the prompt as context. If nothing scores, it falls back to featured projects so the model still has something real to point to.
Why not embeddings? A portfolio has a few dozen items with distinctive names and stacks: "ESP32", "Kotlin", "SmartPot". Keyword scoring finds them, costs nothing per query, adds no extra API call, and when it picks the wrong project I can see exactly why. If the content grows or visitors start describing things in words my pages never use, semantic search is the next step. I wrote about that in semantic search with pgvector in Supabase.
The route also knows which page the visitor is on. A short question like "how does it work?" asked on a project page is treated as a question about that project.
A prompt that keeps it on topic
Most of the work is in the system prompt, not the code. The rules that matter most:
- Only my work. General knowledge, homework help and coding questions about other projects get a short, polite redirect back to the portfolio.
- No meta-discussion. Requests to reveal instructions or hidden context are always refused.
- Real projects first. When a visitor describes an idea, the reply has to name similar work from the context before suggesting anything, plus the two or three things that would decide the scope.
- No prices, no timelines. Never a price, a range, a day rate or a delivery estimate, even when asked directly. Instead it explains what drives cost for that idea and offers to collect a brief.
- Payment rules by region. For clients outside India it mentions only Wise and international bank transfer, using the approximate country from the request.
The pricing rules are the longest section of the prompt, and they spell out the loopholes too: no "starting from" figure, no judging whether a visitor's budget is enough. A made-up range on a portfolio is worse than no answer.
Gemini, with a fallback chain
The model call goes through the @google/genai SDK with a 15-second timeout, a low temperature (0.3) and minimal thinking, because short, factual replies don't need long reasoning. The part I'd recommend to anyone shipping on a hosted model is the fallback list:
- Fyra tries models in order, a current Flash model first and an older one as backup.
- If a model is busy (503), times out (504), is rate-limited (429), has been retired (404) or rejects the request (400), the next model answers.
- An environment variable can replace the list, so when a model is retired I change a setting in Vercel instead of deploying.
This came from experience. On one day every newer Flash model returned busy errors for my key, and the backup answered every message. In a later test, the current first choice answered in about two seconds while a newer model often ran past the timeout, so the order now favours the faster one.
Privacy and cost guards
- Rate limiting per IP, so one visitor can't run up the bill.
- Hashed IPs. The database stores a SHA-256 hash of the IP, never the address itself.
- Selective logging. Messages are only saved once a conversation runs past a few messages or the visitor mentions contact, hiring or a call. Short chats aren't stored at all.
- Validation everywhere. Zod schemas check the chat request and the lead form, including that a budget band matches the currency the visitor was shown.
From chat to brief
When a visitor wants to go ahead, Fyra summarises the brief and points them to "Start a Project Brief", a form inside the chat that collects name, email, location, project type, budget band and timeline. It's stored in Supabase with the conversation, so a first call starts from what the visitor actually needs. Under each reply, the site also shows cards for the projects the reply names, so visitors can jump straight to the case study.
What I'd do
If you're building an assistant for a portfolio or a small business site:
- Start with keyword retrieval over your own data. It's cheap, debuggable and good enough until you have real evidence it isn't.
- Spend your time on the rules. What it must never say (prices, promises, off-topic answers) matters more than clever retrieval.
- Plan for model failures with a fallback list you can change without a deploy.
- Collect structure, not transcripts. A short form at the right moment beats a long chat log.
The same pattern works on other channels too. I wrote about what's realistic for a WhatsApp AI assistant for a shop. If you want an assistant like this for your own site, my AI chatbot page explains how I build them, or see my AI development services.
Frequently Asked Questions
Q:Do you need a vector database for a portfolio chatbot?
Not at the start. A portfolio has a few dozen projects and services, and scoring them by keyword matches on names, taglines and tech stacks finds the right ones for most questions. Embeddings become worth it when the content grows or visitors describe things in words your pages don't use.
Q:How do you stop an AI assistant from answering off-topic questions?
Give it narrow rules in the system prompt, such as discussing only the portfolio, services and matching project ideas to past work, with a set reply that redirects everything else. Fyra also refuses to reveal its instructions and never states prices or delivery times.
Q:What happens when the Gemini API is busy or a model is retired?
Fyra tries a list of models in order. If one returns a busy, timeout, rate-limit, not-found or bad-request error, the next one answers. The list can be changed through an environment variable, so a retired model is a configuration change rather than a new deploy.
Q:Does an AI chatbot have to store every conversation?
No. Fyra hashes the visitor's IP address and only starts saving messages once a conversation runs past a few messages or the visitor asks about contact or hiring. Short, casual chats aren't stored.
Related Engineering Notes
Related Project Cases
Matching Services Tracks
Working on something similar?
Let's collaborate to design custom PCB schematics, write deterministic FreeRTOS threads, or configure secure Next.js databases.