Architecture to Autonomy Back to posts
Architecture & Build Notes

No Database, No Vector Store: How Isvara Was Built as an Agentic System

Isvara was built to make practical private cloud knowledge easier to reach and use during real operational work. It lets people ask questions against the material, use its methods through practical tools, and make those same capabilities available to other agents through MCP. This article looks at the architecture behind Isvara and the decisions that kept the build deliberately thin.

By Samir Roshan 16 min read

I built Isvara around a practical problem: a great deal of knowledge already exists on how to operate a private cloud well, from defining service levels and measuring performance to troubleshooting infrastructure, planning capacity and availability, and managing the environment as a service. The hard part is getting to the right piece of it when you need it.

Much of that experience has been captured by Iwan 'e1' Rahabok in his book Private Cloud Management. Across five parts and roughly 243,000 words, the book covers the operational work in detail: defining meaningful service levels, proving a performance problem with the right counters, and interpreting the metrics behind what users experience as “the system is slow.” That depth makes it a valuable reference. During an incident, though, the useful paragraph may be buried several chapters away. I wanted a faster way into the material without reducing it to a thinner summary that loses the original context.

Isvara started by making that knowledge easier to read, search and question through iO, but it now does more than sit in front of the source material. Several of the methods are implemented as tools for SLA design, performance scoring, capacity planning, failover analysis and cost modelling. MCP exposes the same capabilities to other agents and clients, including asking Isvara, calculating operational scores and working with the underlying knowledge.

Isvara is live at isvara.io and is available to anyone working with VMware and Broadcom private-cloud environments.

The project started under the working name IntelliOps, and the assistant name, iO, came from it. My main constraint was to keep the first version small without painting it into a corner. That meant no database, no vector store, and no multi-agent orchestration unless the product showed a real need for them.

One of Iwan's most useful operating ideas is simple: define what good looks like before you argue about what is bad. I tried to apply the same discipline to the software.

How Isvara is put together

Isvara runs as a single Next.js application on Vercel. The book itself is rendered as static, paginated reading pages, and every heading has a stable anchor so citations can link to a specific section. The agent is exposed through two endpoints: /api/chat for the web surfaces and /api/mcp, a thin MCP adapter over the same backend modules. There is no separate API service or queue, and no data store on the answer path; best-effort telemetry lands in Vercel Blob and a small Redis after the response has streamed. On a question, the server loads the Markdown guide from the deployed bundle and includes it in the model context.

Browser reader · instruments · Ask iO MCP clients Claude · IDEs · other agents POST /api/chat /api/mcp The iO agent web + MCP backend Grounding rules · book · section index Tool calls computeOpsScore · searchTechDocs searchTechDocs TinyFish current Broadcom documentation pickModel Vercel AI Gateway pass-through billing Gemini 2.5 Flash default · 1M context Flash-Lite short questions
Figure 1. Isvara runs the web experience and the agent in one Next.js application. MCP uses the same agent backend, and searchTechDocs reaches current Broadcom documentation through TinyFish.

I kept the runtime small because I did not yet need retrieval, memory or durable workflows. The first thing I wanted to prove was simpler: can the full guide produce useful, source-linked answers at acceptable latency and cost? The interfaces leave room to add those pieces later.

What the stack contains

I kept the stack simple by design. Every component had to be easy to understand, easy to troubleshoot and necessary to the answer path.

01
Framework
Next.js 15 (App Router) + React 19
Chapter pages are generated statically; the agent uses a single Route Handler. The app runs on Vercel with Fluid compute, and maxDuration is set to 300 seconds for long streaming responses.
02
Model layer
Vercel AI SDK v6 + AI Gateway
The agent uses streamText, and every chat surface shares the same useChat conversation. Models are referenced as provider/model strings through AI Gateway, which keeps model changes small and local.
03
Models
Gemini Flash and Flash-Lite
pickModel uses a small heuristic: short lookups and measured-value questions go to Gemini 2.5 Flash-Lite; other questions use Gemini 2.5 Flash, whose 1M context window can hold the full guide. A Sonnet escalation tier exists in configuration but is not currently wired in.
04
Knowledge
Markdown and a section index
The source book is converted from Word to Markdown under content/books. A generated chapter-index.json tracks about 440 sections, and a page manifest maps citations to /read/<part>/<page>#<anchor>. Figures and source .docx files are stored in Vercel Blob rather than git.
05
Guardrails
Prompt rules and injection controls
The guardrails are nine explicit rules in lib/prompt.ts, backed by an injection pre-scan and a per-request spotlight fence (see What broke). The persona, rules, guide and section list are kept in a stable order for provider caching; only the one-line “currently reading” hint changes per request.
06
Instruments
Shared scoring logic and ten instruments
The four-band 0–100 operations score is implemented once in lib/opsScore.ts and reused by the agent, the interactive dial and the MCP server. Eight more tools turn methods from the guide into calculators, builders and blueprints, and the tenth is an explorable canvas of the book’s advanced troubleshooting method.
07
Surfaces
Four chat surfaces, one conversation
Users can ask from the home-page panel, the ⌘I modal, the reader FAB, or a select-to-ask action on highlighted text. All four use one AgentProvider and therefore one conversation. ⌘K remains a separate search palette.
08
Telemetry and evals
Question logs and regression checks
Each question records basic telemetry such as the model used, citation flags and outcome, deferred with after() so it never holds the response stream open. npm run eval replays 20 starter queries and checks for valid citations; npm run eval:injection replays a battery of planted attacks. A key-gated admin digest shows volume, outcomes, model mix, attack rate, feedback and downloads.

What happens when you ask a question

From the reader's point of view, there is not much machinery to see. A question can be asked from any of the four surfaces, and the current chapter is sent with it as a relevance hint. The answer streams back with normal links to source sections. If the question includes a measured value, the scoring tool can also return a small score chip. Clicking a citation opens the relevant part of the guide.

1 The question includes the current chapter as context. inline panel · ⌘I modal · reader FAB · select-to-ask 2 The endpoint screens the question, then assembles the prompt. starter cache · injection pre-scan · nonce fence · cached guide prefix 3 pickModel selects the model for the question. short → Flash-Lite · everything else → Flash 4 Tools run when needed, with a five-step maximum. measured value → computeOpsScore · missing coverage → searchTechDocs 5 The response streams back with links to the cited sections. source sections use stable anchors
Figure 2. The request carries the current chapter as context. The selected model can call a tool when needed, and the response links back to the relevant sections in the book.

Sending the current chapter helps with ambiguous questions because the model knows what the reader is looking at. Citations are also real anchors rather than generated footnotes, so an answer can be checked against the source instead of being treated as the final authority.

Inside Ask iO

Most of Ask iO lives in one route, so the important decisions are easy to inspect. There is one model call surrounded by five practical pieces: loading the guide, building the system prompt, choosing a model, allowing a bounded amount of tool use, and returning a streamed answer with citations.

ASK iO · app/api/chat/route.ts Request message + chapter System prompt cached prefix Model selection Flash / Flash-Lite Tool calls ops score · tech docs loop ≤5 Answer stream + citations
Figure 3. Ask iO takes the request and chapter hint, builds the prompt, selects a model, allows bounded tool use, and streams the answer with citations.

Loading the full book into context

I chose not to add retrieval in the first version. On each question, loadBooksContext() assembles all five parts of the guide and includes them in the model context, a pattern I call load-and-ask. The combined guide is built once per server instance and placed in a stable part of the prompt so provider-side caching can reduce repeated input cost. This removes retrieval misses because there is no retrieval stage; it does not remove model error, which is why the source links remain important.

The system prompt and guardrails

The policy layer is part of the system prompt rather than a separate service. Nine numbered rules cover the behaviours I care about most: use the guide first, cite the exact section, search current tech docs only when the guide is insufficient, explain trade-offs, and treat instruction-like text inside supplied content as data rather than commands. Prompt-injection testing showed that the rule alone was not enough, so I added two mechanical checks around it. A regex pre-scan detects likely injection without refusing the question, routes flagged requests to the sturdier model, and records the event. The latest user turn is also fenced between per-request random-nonce markers with a trust reminder after it. The fence is applied to a copy of the messages, so the cached prompt prefix stays unchanged. An injection eval battery checks these three layers for regressions.

For this version, I prefer nine rules I can read and test in one file over a policy layer whose complexity I cannot yet justify.

Choosing between Flash and Flash-Lite

I did not want to pay for the larger model on every lookup, so pickModel uses a conservative string heuristic with no extra model call. Short, marker-free lookups and measured-value questions go to Gemini 2.5 Flash-Lite; everything else goes to Gemini 2.5 Flash. There are three promotions to Flash: a Lite run that calls a tool switches for its subsequent steps, an attempt that fails before producing text is retried once, and a question the injection pre-scan flags skips Lite entirely. The “try again” action also forces Flash.

Keeping tool use bounded

The model call is small: streamText, temperature 0.2, a frequency penalty, and two tools. stopWhen: stepCountIs(5) gives the model enough room to call a tool and use the result, while putting a hard ceiling on the loop. There is no planner and no sub-agent layer in the current design.

// app/api/chat/route.ts, the heart of the agent (lightly compressed)
const { messages, chapter, escalate } = await req.json();
const scan = scanForInjection(latestUserText(messages));   // detect only, never refuse
const model = escalate || scan.flagged ? MODEL_PRIMARY : pickModel(latestUserText(messages));

const result = streamText({
  model,                                   // 'google/gemini-2.5-flash' or '-lite'
  system: systemPrompt(loadBooksContext(), chapterListForPrompt(), chapter),
  messages: convertToModelMessages(
    spotlightLastUserMessage(messages),    // nonce-fence the latest turn only
    { ignoreIncompleteToolCalls: true }),
  tools: { computeOpsScore, ...(hasTinyFishKey ? { searchTechDocs } : {}) },
  stopWhen: stepCountIs(5),                // bounded loop: tool, then answer
  prepareStep,                             // a tool call promotes Lite → Flash
  temperature: 0.2, frequencyPenalty: 0.5, maxOutputTokens: 4096,
});

return result.toUIMessageStreamResponse();

Sharing the scoring logic across Isvara

The operations score is shared application code. computeOpsScore takes a metric and measured value, applies the thresholds from the guide (CPU ready 2.5%, memory latency 1.0%, disk latency 10ms, zero dropped packets), and returns a 0–100 score across four bands using 100·(1−value/(4·threshold)). The Tools page uses the same function, so there is only one implementation of the scoring logic. The other agent tool, searchTechDocs, is used when the guide does not cover the question. Tool results can render as score chips, and citations appear as “Read” links into the guide.

// lib/tools.ts, the two instruments the agent can call
export const computeOpsScore = tool({
  description: 'Compute the four-band operations score for a measured signal...',
  inputSchema: z.object({
    metric: z.enum(['cpu_ready', 'mem_latency', 'disk_latency', 'dropped_packets']),
    value: z.number().min(0),
  }),
  execute: async ({ metric, value }) => scoreFor(metric, value),  // lib/opsScore.ts
});

export const searchTechDocs = tool({
  description: 'Search Broadcom tech docs for topics the guide does not cover...',
  // pass 1: site:techdocs.broadcom.com · pass 2: wider web, only if pass 1 is empty
  // 7s timeout, one retry, up to 5 results with exact page URLs
});

Using TinyFish for current documentation

The guide will inevitably lag the product. VCF releases continue after a document is published, so some questions cannot be answered reliably from the bundled material alone. searchTechDocs handles that case by searching current documentation when the guide does not cover the topic.

TinyFish powers that search. Isvara queries techdocs.broadcom.com first and falls back to a wider web search only when needed. I use it here because it returns structured results with page URLs the agent can cite directly. For questions outside the bundled guide, I would rather return a checkable source than have the model manufacture a plausible-looking reference.

Web search was easy to add. The harder requirement was making sure every external answer points to a URL a reader can open and verify.

I kept the integration small: two search passes, a short timeout, one retry, and a clean “search unavailable” result if the key is missing. If external search fails, that part of the answer can fail without taking down the chat endpoint.

What the ten instruments do

I did not want the book to become chat-only. The Tools section turns ten of its methods into things you can use directly, grouped into four jobs: define the target, measure the environment, plan capacity or cost, and diagnose problems. Each tool shows whether it is based only on the guide or also uses current tech documentation, and the scoring tools reuse the same code as the agent.

A
Define good
SLA Builder · Operations Maturity
Define service classes and four SLIs, export the resulting SLA, and use a 15-question assessment across five operational areas to understand current operations maturity.
B
Measure and operate
Ops Score · Cluster KPI · Super Metric · Dashboards
Includes the four-band operations score, a cluster KPI engine with progressive weighting, a VCF Operations super-metric builder, and dashboard blueprints for different operational roles.
C
Capacity and cost
Capacity · Failover · Cost
Covers VMs-per-host using allocation, reservation and utilisation models; admission-control reserve and post-failure headroom; and a private-cloud cost comparison against a public-cloud ceiling.
D
Troubleshooting
Advanced Troubleshooting
Advanced Troubleshooting is different from the other tools because it does not calculate anything. It presents the book’s troubleshooting method as fifteen linked cards, with guided navigation and a citation on every card back to the guide.

From source documents to a working system

I built it in five usable increments, and kept shipping after launch. Each stage left the site in a working state before I added the next layer.

M0 · Ingestion
Word to Markdown, figures to Blob
Pandoc converts the .docx sources to Markdown, generates a chapter-index.json with about 440 stable section anchors, and extracts roughly 1,200 figures to Vercel Blob. Getting this conversion right matters because every later citation depends on it.
M1 · The endpoint
Full-context agent with prompt rules
Build the /api/chat handler, load the guide into context, order the stable prompt for caching, and add the nine rules. This was the first point where the agent could answer from the guide with source links.
M2 · Surfaces
Panel, modal, FAB, select-to-ask
Connect the four chat surfaces to the endpoint, add streaming and citations, and keep them on one shared conversation. In parallel, paginate the reader and add the first nine tools.
M3 · Guardrails + evals
Add evals, telemetry and budget controls
Tighten the prompt rules, add a 20-query evaluation set that checks for valid citation links, record per-question telemetry, and set a Gateway budget cap. I track the cost of a completed answer rather than raw token count.
M4 · Deploy
Deploy isvara.io and add llms.txt
Deploy the Vercel project, configure AI Gateway, ensure the Markdown is included in the chat function bundle, attach the domain, and add public/llms.txt so other agents can understand the site.
M5 · Post-launch
Add MCP, injection tests and advanced troubleshooting
Expose the agent through an MCP server at /api/mcp, layer the prompt-injection defence and its eval battery, add the Advanced Troubleshooting canvas, a downloads page for the original sources, and a key-gated usage digest.

Keeping the guide current

The guide changes over time, so the ingestion path also handles refreshes. The source .docx files live in Vercel Blob. A weekly GitHub Action compares their hashes with a manifest; when something changes, it reruns the conversion, rebuilds the index, uploads any changed figures, commits the Markdown, and triggers a redeploy. Updating the content is therefore just a matter of replacing the source document in Blob.

Blob .docx source documents Check source hash weekly GitHub Action Convert + reindex Pandoc → Markdown Commit Markdown + index Deploy Vercel
Figure 4. A weekly GitHub Action checks the source hashes. When a document changes, the pipeline reconverts it, rebuilds the index, commits the updated content and redeploys the site.

The cost of sending the full book

Sending the full guide has one immediate cost: input tokens. A question carries a few hundred thousand tokens before the model generates an answer, so an uncached Flash request is roughly a tenth of a dollar at the pricing used for this build. Provider caching makes a big difference because the stable guide prefix is usually the expensive part; cached requests can fall to a cent or two. Short questions are routed to Flash-Lite, and the four starter questions are served from a cache without a model call. If input cost starts to dominate, the next options are explicit caching, selecting only the relevant book, or moving to retrieval.

What broke, and what I changed

The finished diagram hides some of the problems I hit while building it. Each of these five left a concrete change in the current code.

The cheap model would not stop talking

Flash-Lite was attractive on price, but tool use exposed a repeatable failure mode: it would finish an answer, call the tool, and then restate the answer from the beginning. Routing longer or more complex questions to Flash solved most of that behaviour, with a frequency penalty as an additional backstop. pickModel exists largely because of this test result.

One chapter was a hundred and forty thousand words

The first reader rendered one page per book. One page ended up at roughly 140,000 words of Markdown, which was slow on desktop and unpleasant on a phone. The current reader splits books by chapter and splits the largest chapters again at subheadings, keeping pages to roughly 25,000 words or less. Citations now resolve to the exact page, and old anchor links redirect to the page that owns the section.

Invisible whitespace crashed the page

Some merged-cell tables do not convert cleanly to Markdown, so they remain as HTML. React hydration failed when whitespace appeared between certain table tags. The fix belongs in ingestion: the converter now emits those tables as a contiguous HTML block with no inter-tag whitespace, which removes the runtime failure instead of working around it in the reader.

A dropped connection poisoned the next question

If a reader navigated away during a tool call, the conversation could retain an incomplete function call. Replaying that thread on the next question caused a provider 400. The endpoint now ignores incomplete tool calls during message conversion and buffers the opening chunks so a failure before any text can be retried once on Flash.

A crafted paste walked straight past the rules

Prompt-injection testing produced one live failure: a template-wrap attack pasted into a question got Flash-Lite to obey the planted instruction. I did not try to fix that by adding another paragraph to the prompt. I kept the semantic rule and added two mechanical checks: a regex pre-scan that detects likely injection without refusing anything, routes flagged questions to Flash and records them, plus per-request random-nonce markers around the latest user turn that a pasted document cannot forge. The injection eval battery catches regressions, and the admin digest tracks the live attack rate. Some template-wrap attacks still succeed on Flash. I accept and monitor that residual rather than add another model call or block suspicious-looking questions outright.

Why Isvara is public

The vCommunity has always been generous with knowledge, scripts, troubleshooting experience and practical guidance. Iwan and I wanted Isvara to contribute in the same spirit, which is why it is available as a free public resource at isvara.io with no authentication gate. The reader is paginated and usable on a phone, every heading has a deep link that can be dropped into a ticket or shared with a colleague, and the downloads page includes the source documents, diagrams, sample reports and spreadsheets. public/llms.txt gives other AI systems a machine-readable description of the site.

Isvara also exposes an MCP server at isvara.io/api/mcp, so Claude Desktop, IDEs and other agents can use the same capabilities as the web application. ask_isvara uses the same grounded question-answering path and citations, while ops_score calls the scoring logic directly without using a model. The section index, thresholds and guide are also available as MCP resources. I reuse the same backend modules for both the web app and MCP, rather than maintaining separate implementations. I only expose a capability through MCP once its underlying logic has been separated from the page code, as I did with the operations score. The MCP page has the setup details.

Isvara has also been shaped by the people around it. Vincent Han has been part of the discussions and ideas throughout, and has since taken the build on-prem, running it on Kubernetes with VKS, the vSphere Kubernetes Service. How the same architecture behaves on a private-cloud platform instead of Vercel is a story for a future post.

What I left out

A few components you might expect are missing from this version. I left them out because the current workload does not justify them yet.

When full-context stops making sense

Full-context prompting works well here because the corpus is bounded, changes infrequently, and fits inside the model window. It also avoids a retrieval stage, so there is no risk of selecting the wrong chunk before generation. That does not make it universally better: the model can still misunderstand the material, and the input cost is real. I would add retrieval when the corpus no longer fits comfortably, changes too often to cache effectively, or traffic makes full-context input materially expensive. Starting with a vector store by default would have added infrastructure before I had evidence that retrieval was the actual problem.

Two additions are the most likely next steps:

  • Retrieval, memory, and durable workflows. If the corpus or traffic grows, hybrid retrieval with a re-ranker over Postgres/pgvector could replace full-guide prompting. Longer-running operational methods could then become persisted workflows, with their execution history serving as the evidence trail. Memory would be a separate concern rather than something hidden inside chat history.
  • A heavier model for the hardest questions. A Sonnet escalation tier is defined in the model configuration but is not wired in yet. If the eval set or question log shows a class of questions the routed Gemini pair handles poorly, it can be added behind the existing router without changing the rest of the architecture.

That is the approach I have taken with Isvara: start small, watch where the pressure appears, and add infrastructure only when the workload needs it. At this stage, being able to inspect and change the whole system easily is more valuable than adding another layer.

The bottom line

That is where Isvara stands today. It runs as one application, with the same agent available through web chat and MCP. Iwan's full guide stays in context, two agent tools handle scoring and current documentation, and ten operational instruments sit alongside the reader. Retrieval and memory can come later if usage justifies them. For now, keeping the system small makes its behaviour, cost and failure modes easier to understand.

Views expressed here are my own and do not represent the views of any current or former employer, client, or affiliated organization.