From Portfolio Website to AI-Operable Engineering Platform

How the same database rows became evidence for cover letters, a corpus for Q&A, and a surface for MCP tools

AI Systems·Advanced·7 min read · July 6, 2026

The Beginning

I built this portfolio in October 2025 to do one thing: show my work. A page, a few sections, work experience and skills rendered from whatever I'd hardcoded that week. It was static in every sense that mattered — content lived in the markup, and updating it meant editing a file and redeploying.

That was fine. It was fine right up until it wasn't editable enough for me to want to touch it, and I started avoiding small updates because "redeploy the whole site to fix a typo in my job title" is not a workflow anyone enjoys for long.

The First Solution

So in January 2026 I gave the site a database. SQLite behind a small admin CRUD surface, gated by authentication a few weeks later. Work experience, education, skills — all editable from a form now, no redeploy required. This felt like the obvious fix, and it was: it solved exactly the problem I had, which was "I want to edit my own content without touching code."

For about four months, that was the whole story. A portfolio with a content management layer. Nothing about it needed to be smarter than a form and a table.

What Broke

The break came in May, when I decided I wanted the site to do something a static/CRUD site fundamentally can't: take a job description and generate a tailored cover letter from my actual work history. Then, weeks later, the same shape of problem showed up twice more — I wanted visitors to ask questions about my engineering decisions and get real answers, and I wanted a photo library that could be searched by meaning, not just filename.

Each time, I ran into the identical wall. The database I'd built was designed to answer one question — "what should render on this page" — and none of these new features are that question. A cover-letter generator needs to score which pieces of experience are actually relevant to a specific job description. A Q&A surface needs to retrieve evidence for a claim, not just display a static bio. A photo search needs embeddings, not a WHERE clause. None of that existed, and worse, none of it could be bolted onto the existing tables without a real risk I cared about a lot: an LLM producing prose that quietly drifted from what the database actually said. A cover letter that invents a project. A CV that reorders my dates for a nicer story. Those aren't formatting bugs — they're the system lying about me to an employer.

The Decision

The tempting move here was to treat each of these as "add an AI feature" — bolt a Claude call onto the relevant page and see what happens. I rejected that almost immediately, because I'd already watched what happens when you hand a model a pile of loosely relevant context and ask it to write persuasively: it doesn't stay quiet about the gaps, it fills them with something plausible. I didn't want three independent bolt-ons, each trusting Claude a little more than the last out of accumulated laziness.

Instead, I made the database the substrate every subsystem reads from, and drew the same line through all three: a deterministic half computes and scores what's true and relevant, and only that pre-selected, bounded result is ever handed to Claude, whose job is narrowed to prose. Cover letters and CVs, engineering Q&A, and photo search and enrichment all ended up with the same shape, even though they were built months apart and for different reasons — because the constraint (don't let a model decide what's true) doesn't change per feature.

That's a real trade-off, and I don't want to pretend it wasn't. Making the same tables serve rendering and retrieval means schema changes now ripple further than they would on a purely presentational site — which is exactly why db:init only ever runs CREATE TABLE IF NOT EXISTS and never a destructive migration; I didn't want a schema tweak for one subsystem to have a chance of quietly breaking another.

The New Architecture

Static content ↓ SQLite + admin CRUD (editable, not yet reasoned over) ↓ Three deterministic pipelines, one shared principle ↓ Cover Letter / CV Ask Vikram Q&A Photography Search ↓ ↓ ↓ scored evidence retrieved decisions embedded photos ↓ ↓ ↓ Claude — prose only, bounded input ↓ MCP tools (same pipelines, callable outside the browser)

Once the cover-letter and CV pipelines existed as isolated, named functions rather than a page's worth of inline logic, exposing them as MCP tools was almost incidental — a thin adapter layer over work that already had to exist. That's not something I planned from the start; it's something the earlier decision to keep pipeline logic out of routes and components made nearly free later.

Lessons Learned

What surprised me most is how early the MCP step arrived once the pipeline was function-shaped — I added it within a day of the pipeline working at all, not as a mature feature but because there was almost nothing left to build. I'd assumed exposing tools to an external client would be its own project. It wasn't, and that told me the earlier decision — isolate pipeline logic, keep it out of route handlers — had already done the hard part without my realizing it at the time.

I also got something wrong: I let documentation lag the architecture. mcp/README.md still describes bearer-token auth as the primary path months after the actual system moved to OAuth 2.1 with GitHub login. It's a small drift, but it's the exact failure mode this whole platform exists to prevent elsewhere — a place where what's written and what's true quietly diverge — and it happened in my own docs before I caught it.

Looking Forward

The engineering_decision table — the corpus Ask Vikram retrieves from — still doesn't have the authoring tooling the blog/journal content just got. It's a smaller, less-exercised admin surface than the content types I touch constantly, and that imbalance is worth fixing before the corpus grows large enough for it to matter more.

Takeaway

A portfolio doesn't become a platform because you added an AI feature to it. It becomes one when the same rows in the same tables turn into something multiple deterministic pipelines can read and reason over, each behind its own boundary — a public page, an admin dashboard, a retrieval pipeline, and now an MCP client, all pointed at data that was never written with any of them specifically in mind.