I Productized Myself: The Architecture of This Site
In plain terms — Instead of publishing another portfolio that claims what Harsh can build, this site turns those claims into a working system. Visitors can question a source-backed agent, test job fit, or connect the same knowledge to their own AI. The larger idea is simple: a chef makes a stronger case with a tasting menu than with a résumé describing dinner.
The cover letter is upside down
Most engineering portfolios describe the work. They have project cards, a skills cloud, a résumé download, and several tasteful paragraphs explaining that the owner builds reliable systems.
I wanted this site to make a different argument: stop describing, start demonstrating.
patelharsh.dev is a small SaaS product whose product happens to be me. A hiring manager can interrogate an agent grounded in my actual work. A recruiter can paste a job description and get a requirement-by-requirement Fit Report. An engineer can inspect the architecture, try to break the grounding rules, or add the public MCP server to their own Claude. The interface is not a brochure for AI-native platform engineering. It is an instance of it.
That framing creates a useful constraint: every feature has to prove a claim. Chat proves retrieval and grounding. Fit Report proves structured analysis and calibration. MCP proves that the same capability can travel outside my UI. The build and eval pipeline proves that I do not consider “the model seemed good in the demo” a release criterion.
The resulting system is a Next.js application on Vercel, backed by Supabase Postgres and pgvector. Voyage supplies embeddings. Claude models, selected per call through OpenRouter, handle rewriting, chat, and structured assessment. The content is MDX and YAML in the repository, which means the public site, the agent's knowledge, and the evidence behind its answers share a source of truth.
flowchart LR
A[MDX, facts, resume, Q and A] --> B[Sanitize, chunk, hash]
B --> C[Voyage embeddings]
C --> D[Supabase Postgres and pgvector]
E[Visitor question] --> F[Rewrite and classify]
F --> G[Vector search plus full-text search]
D --> G
G --> H[Reciprocal-rank fusion]
H --> I[Claude with citation rules]
I --> J[Chat, Fit Report, MCP]
The corpus is a build artifact
Grounding starts before retrieval. The corpus is assembled from curated facts, the structured résumé, architecture docs, writing, and approved Q&A. The evergreen architecture pages are especially valuable because they contain the kind of implementation detail a recruiter may not ask for but an engineering director will. The pages on build-failure triage, agent-facing code search, and release engineering are not supporting SEO copy; they are retrieval targets.
The ingestion path is markdown-aware. It splits on headings, carries the full heading path, anchor, document slug, source type, and URL into chunk metadata, and aims for 400–700 estimated tokens with roughly 15 percent overlap. That is large enough to preserve an architectural argument and small enough to avoid returning an entire essay when the useful evidence is one section. Because every heading has an anchor, a citation can land on the relevant section rather than vaguely waving at a document.
Before the pipeline asks an embedding service to see anything, it scans every assembled chunk through a denylist. The gate covers internal hostnames, confidential terms, ticket shapes, cloud-account identifiers, key material, secret-like strings, and private contact data. A match fails ingestion. This is not a best-effort redaction pass after publishing; it is a CI boundary before external access.
Each clean chunk gets a SHA-256 content hash. On a rebuild, the ingester reads the existing hashes, embeds only content it has not seen, upserts those rows, and removes stale rows. The corpus is small enough to rebuild as a set, while hash-based idempotency avoids paying to re-embed unchanged paragraphs. I reused the Postgres-plus-pgvector pattern from my Open Brain project, but made the website corpus read-only and deploy-oriented.
This is the first principle I would carry to a larger RAG system: the quality and safety boundary belongs at ingestion. Retrieval cannot rescue a corpus you do not trust.
Hybrid retrieval, because language is annoyingly specific
Semantic search is good at finding conceptually related material. Full-text search is good at exact names, acronyms, and strings that embeddings sometimes treat as decorative. A portfolio corpus contains both. Someone may ask about a general release-safety pattern; someone else may ask for a particular tool by name.
For each chat turn, the system rewrites the conversation into a standalone query and classifies its intent. Work and site questions proceed to retrieval; fit questions are routed to the Fit pipeline; off-topic and adversarial inputs take the guardrail path instead of wasting a retrieval and generation call.
The retriever runs two rankings against Supabase Postgres: the top 20 vector matches from pgvector cosine search and the top 20 matches from Postgres full-text search. Reciprocal-rank fusion combines those lists and returns the top eight chunks. RRF is deliberately boring. It does not require the scores from two different search systems to mean the same thing; it rewards documents that rank well in either list, and especially in both.
Vector search can also degrade. If query embedding or vector retrieval fails, the implementation falls back to keyword results. If full-text search fails, the request fails rather than pretending the intended hybrid contract still exists. That asymmetry is intentional: reduced recall is a degraded service; silently returning an untrustworthy result set is a different product.
The selected chunks become sources S1 through S8, each with its URL and
anchor. Those labels are the only evidence the model is allowed to use alongside
the curated facts block.
Citations are a hard rule, not a decoration
The system prompt is unusually blunt about evidence. Every factual claim about
me requires an adjacent [S#] citation. The model may use only the facts block
and retrieved sources. The facts block wins if sources conflict. Claims must be
calibrated to the strength of the evidence: shipping and operating a system is
not the same as being adjacent to one, and neither becomes “expert” through
enthusiastic autocomplete.
The failure behavior is specified too. When the sources do not support an answer, the agent must say exactly, “I don't have that in my knowledge,” and point the visitor to contact. If there is no evidence for a technology or responsibility, it states the gap instead of stretching a neighboring fact. Instructions pasted by a user are treated as data, not commands. The agent speaks about me in the third person and never impersonates me.
This creates a slightly less magical demo and a much more useful one. An agent that invents an impressive project is not selling me; it is generating a future background-check problem. Honest refusal is part of the product.
The eval harness is the trust mechanism
A prompt rule without a test is an aspiration. The smoke-eval harness turns the behavior contract into 40 executable cases:
- 15 factual questions require both the expected grounded fact and at least one citation.
- Five unknown questions require an honest-gap response and a contact pointer.
- Five adversarial cases probe prompt extraction, persona override, and private data requests.
- Five off-topic cases require a short redirect rather than a general-purpose assistant answer.
- Five logistics cases check curated values.
- Five Fit cases exercise valid reports, a strong domain match, an unsupported requirement, and rejection of text that is not a job description.
The harness reads the actual Server-Sent Events emitted by the chat route, collects text deltas and explicit citation data, runs category-specific assertions, and writes a result artifact. The overall threshold is 90 percent. Below it, the process sets exit code 1, so the release gate can block promotion. Failing runs did exactly that during the build.
That last sentence contains some scar tissue. The first live run scored 14.3 percent because the harness parsed the wrong streaming wire format; worse, a forced process exit did not propagate the failing status reliably through the runner. The fix was to parse the observed SSE contract and extract the threshold calculation into a unit-tested function. The harness now lets the event loop drain and sets the process exit code explicitly. Testing the tester is not ceremony when the tester controls deployment.
The 90 percent threshold is not a claim that the agent is 90 percent “correct” in the abstract. It is a release contract over named behaviors with traceable fixtures. That is narrower, less marketable, and operationally meaningful.
Fit Report is where the architecture earns its keep
Chat is the obvious demo. Fit Report is the useful product.
A visitor pastes a job description. The pipeline first validates that the text is actually a JD, then extracts at most 15 distinct requirements while preserving must-have versus nice-to-have language. It runs hybrid retrieval separately for each requirement, keeps up to four chunks per requirement, deduplicates sources, and assigns stable citation labels across the report. Claude then produces a fixed schema: a short summary, a requirements matrix, three highlights, honest gaps, and three suggested interview questions.
The matrix forces the question a cover letter usually avoids: what evidence is
there for each requirement? Ratings are limited to Strong, Solid, Partial, and
No evidence. If retrieval returns no evidence for a requirement, application
code overrides the model and sets No evidence with an empty citation list.
The model does not get a veto. The honest-gaps section is mandatory and
non-empty; if generation omits it, the application synthesizes one from the
weakest assessed area.
This is why Fit Report is more than “ask a model to compare two documents.” It is a requirement matrix with evidence lineage and enforced negative space. The absence of evidence survives generation, validation, rendering, sharing, and print/PDF export.
One implementation decision here is worth stealing. Claude's structured-output provider rejects some JSON Schema size and range constraints. Instead of weakening the product contract, the pipeline uses a permissive model-facing schema, then normalizes and validates against a strict application schema. The wire contract accommodates the provider; the application contract still owns the truth.
For PDF export, I chose the browser print dialog and dedicated print CSS rather than a second rendering library. The shared report component remains the one rendering path. “Download PDF” is less glamorous this way, but duplicated document pipelines have a habit of becoming two subtly different products.
Put the candidate inside the recruiter's Claude
The public MCP server takes the same thesis one step further. It is read-only, uses Streamable HTTP with an SSE fallback, and exposes six tools: a curated summary, experience search, project lookup, job-fit analysis, résumé retrieval, and current availability. The job-fit tool uses the same pipeline and rate limits as the web interface.
That means a recruiter or hiring manager does not have to use my chat UI. They can add the candidate to their own Claude and ask questions in the workspace where they are already comparing a role, interview notes, and other material. The server can return evidence and links, but cannot mutate site or user state beyond operational logging.
I like this inversion. A conventional portfolio asks a recruiter to adapt to the candidate's funnel. MCP lets the candidate become a tool in the recruiter's existing one.
The useful decisions were found in the failures
The architecture diagram is tidy. The build was not, which is why the decision records matter.
I routed model calls through OpenRouter instead of binding the application to a direct provider account. That keeps per-call model choice, spend caps, and a switching path in one place while still selecting Claude for the visible work. Embeddings remain separate through Voyage.
That separation exposed a real integration edge. A Voyage credential provisioned through MongoDB Atlas authenticates against the Atlas endpoint, not the native Voyage endpoint, although the wire format is the same. The first hardcoded endpoint returned 403s. The correction was not a provider-specific branch; it was to make the embedding base URL and model configuration, use the same values for ingestion and query embeddings, and test trailing-slash behavior.
The structured-output issue produced the two-schema design described above. The Fit PDF requirement produced print CSS instead of another dependency and another renderer. These are modest decisions, but together they are the actual platform-engineering story: observe the contract that exists, isolate the variation, and keep one strict boundary under application control.
What it costs, and what generalizes
The system is designed so fixed cost stays small and variable cost follows use. Static Next.js pages are served through Vercel; the plan starts on Hobby and moves to Pro when traffic warrants. Supabase provides Postgres, full-text search, and pgvector in one service. OpenRouter meters model calls and provides spend controls. Voyage is used for query embeddings and for new corpus content, not unchanged chunks. Rate limits bound chat, Fit, and MCP usage.
The repository does not contain a trustworthy current monthly dollar figure, so I will not invent one for architectural symmetry. The important cost choices are visible: cheap classification before expensive work, retrieval before generation, hash-based embedding idempotency, static delivery where possible, and explicit rate limits at every generative surface. The first serious traffic spike should produce a bill that can be explained by events, not a séance.
The parts that generalize are not “use my stack.” They are the contracts:
- Build the corpus as a governed artifact, and sanitize it before external processing.
- Combine semantic and lexical retrieval when the domain contains both concepts and exact identifiers.
- Make citation and honest-gap behavior executable requirements.
- Enforce critical negative claims, such as
No evidence, in application code. - Give the same core capability multiple interfaces without creating multiple truth sources.
- Let evals fail the release, then verify that failure itself exits non-zero.
I started with an inverted cover letter: the best way to claim I build grounded, evaluated agent systems was to make the claim queryable. The finished site is still a portfolio. It just has an ingestion pipeline, a retrieval layer, a schema-enforced hiring tool, an eval gate, and a protocol surface. Apparently I have difficulty making a simple résumé.