Anjo AI
Starting up…
Case study
A grounded, cited AI assistant with no database, running on Vercel for well under a dollar a month.
How this portfolio was designed as a retrieval-augmented AI assistant over Markdown — grounding rules, hybrid retrieval, a zero-infrastructure fallback, cost controls, and an evaluation suite — and what the trade-offs were.
Most developer portfolios are pages a recruiter scrolls through. The goal here was different: make the portfolio itself an assistant that a recruiter, engineer, or collaborator can simply ask — "What is his .NET experience?", "Tell me about Asterweave", "How can I contact him?" — and get a fast, honest answer with sources. It had to be production-grade (security, cost control, accessibility, mobile) rather than a chatbot widget bolted onto a static site, and it had to be honest: an assistant that invents an employer or a credential is worse than no assistant at all.
The project was delivered in a single, tightly scoped build on top of an existing Next.js 16 scaffold, with the Asterweave repository-analysis agents used for the initial codebase discovery.
Three problems had to be solved at once.
Markdown files under /content carry Zod-validated frontmatter (type, visibility, featured, structured facts such as profile identity, skill groups, and project links). A content pipeline normalises each file (line endings, NFKC, stripped zero-width and bidi control characters, stripped HTML comments), splits it into heading-aware chunks with deterministic ids (slug::section::ordinal) and SHA-256 hashes, and embeds only the chunks that changed.
At query time the request flows through validation → rate limiting → deterministic query understanding (intent classification and follow-up rewriting, no extra model call) → hybrid retrieval → a grounded prompt in which the retrieved sections are numbered and fenced as untrusted data → a streamed answer → server-derived citations, typed cards, and related questions.
Pages (/about, /projects/[slug], /skills, /case-studies/[slug], ...) are prerendered from the same repository module, and a visibility: private document can never reach a page, the index, search, or the model.
KnowledgeRetriever is what the app depends on; VectorStore is what a backend implements. Three stores exist: a JSON file index (the production default, loaded into memory), PostgreSQL + pgvector (optional, for larger corpora), and in-memory (tests).asp.net/c#, synonym expansion at reduced weight, a BM25F-style metadata field, and a title boost — nails exact terms like ".NET" and "pgvector". Results are fused with reciprocal rank fusion (k = 60) and floored on relevance.fetch) when a key is configured; otherwise an extractive provider that quotes the top retrieved sections verbatim with citations through the same wire protocol — so the site is useful with no key at all.[n] markers.data/knowledge-index.json: ids, hashes, and vectors — no text). Brute-force cosine over a few hundred vectors is sub-millisecond, deployments need only an API key, and CI fails if the committed index is stale. pgvector remains one interface implementation away.fetch, not an SDK. Two endpoints, full control of timeouts and abort, no extra bundle weight, and upstream error bodies are never surfaced (they can echo prompt content).<dialog> instead of a component library. Focus trapping, Escape, inert background, and top-layer rendering come for free.Zod on every boundary (frontmatter, requests, environment); byte-counted body limits; same-origin enforcement on POST; per-minute and per-hour rate limits keyed by the trusted proxy hop; CSP, HSTS, and frame/MIME/referrer headers; Markdown rendered without raw HTML, with a URL allowlist and no images; dangerouslySetInnerHTML used exactly once, for JSON-LD from validated frontmatter; retrieved content fenced with unpredictable per-request delimiters; sanitised structured logs that never contain prompts, answers, or secrets; a secrets scanner as the first CI step and a local pre-push hook.