6 min read

I built a RAG endpoint for my portfolio and skipped the vector database

Retrieval is a fix for a corpus that doesn't fit. Mine is under three thousand tokens, so the retrieval step was pure cost — here's the reasoning, and what I built instead.

RAGClaude APINext.jsArchitecture

Every RAG tutorial opens the same way: chunk your documents, embed the chunks, push them into a vector store, embed the query, pull the top k, stuff those into the prompt. I have built that pipeline. For this site, I did not build it — and the reason is worth writing down, because the reflex to reach for retrieval is strong enough that I nearly did it anyway.

Retrieval solves a problem I did not have

Retrieval exists because your corpus does not fit in the context window, or because sending all of it on every request costs more than you want to pay. Both are real problems. Neither is my problem. The entire answer corpus for this site — six project case studies, the FAQ, my full timeline, education, and hackathon record — is about 2,600 tokens. It fits in a 1M-token context window with room to spare roughly four hundred times over.

So what would retrieval actually buy me? A smaller prompt. And what would it cost? That is the part the tutorials skip.

What top-k retrieval would have broken

The questions people ask a portfolio are frequently comparative. Which of these projects used PostgreSQL? What has he actually shipped to production versus built for a hackathon? Does he have backend experience outside of Node? Every one of those questions needs the whole corpus in view at once. Top-k retrieval answers them by pulling the three chunks that look most similar to the query and dropping everything else — which is exactly how you get an answer that is fluent, specific, and wrong by omission.

There is a second cost that is easy to underrate: an index is state, and state drifts. Every time I add a project I would need to re-embed and re-index, or the assistant silently answers from a stale corpus. That is a whole category of bug I would be introducing in exchange for a shorter prompt.

Prompt caching removes the cost argument

The remaining reason to retrieve is money: resending the corpus on every request adds up. Except prompt caching already solves that, and more cleanly than retrieval does. The corpus goes into the system prompt behind a cache breakpoint. The first request writes the cache at a small premium; every request after that reads it at roughly a tenth of the input price.

The one rule that makes this work is that caching is a prefix match. Anything that changes invalidates everything after it, so the stable corpus has to come first and the visitor's question has to come last:

const SYSTEM_BLOCKS = [
  { type: "text", text: SYSTEM_INSTRUCTIONS },
  {
    type: "text",
    text: `<profile>\n${buildCorpus()}\n</profile>`,
    // Breakpoint goes on the corpus — the part that never changes.
    cache_control: { type: "ephemeral" },
  },
];

// The question rides in `messages`, after the cached prefix.
const stream = client.beta.messages.stream({
  model: "claude-opus-5",
  system: SYSTEM_BLOCKS,
  messages: [{ role: "user", content: question }],
});

Get that backwards — interpolate the question into the system prompt, or put a timestamp above the corpus — and you write a fresh cache entry per visitor and never read one. The failure is silent. Nothing errors; you just quietly pay full price forever. The way to check is to read the usage back:

// If cache_read_input_tokens stays at 0 across repeated
// requests, something upstream of the breakpoint is changing.
console.log(final.usage.cache_read_input_tokens);

Grounding, and the part I care about most

The corpus is assembled at build time from the exact same modules the pages render — the projects array, the FAQ array, the timeline. That is not a convenience, it is the guarantee. The assistant physically cannot state a role, a date, an award, or a tech stack that the site does not also display, because there is no second copy of that information for it to drift from.

The system prompt then closes the remaining gap, which is invention. A model asked about latency or uptime will happily produce a plausible number. I have not measured or published any, so the instruction is explicit that it must not estimate them, and that questions the corpus does not answer get an admission and my email address rather than a guess.

An assistant that says "that isn't on the site, email him" is more useful to a recruiter than one that invents a confident answer. The invented one costs me the interview when it turns out to be wrong.

When I would reach for a vector store

None of this is an argument against retrieval. It is an argument for checking the size of your corpus before you architect for one you do not have. I would reach for embeddings the moment any of these were true: the corpus stops fitting in context, or grows past the point where paying for the full prefix on a cache miss hurts; the questions become lookups over many independent documents rather than reasoning across one small body of facts; or the content changes often enough that a build-time assembly step stops being viable.

For a personal site with six projects, none of those hold. The simplest thing that works is a cached prompt and a corpus built from the source of truth — about ninety lines total, no index to maintain, and nothing that can quietly fall out of sync with the pages themselves.