<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Virginia Nyambura  Mwega</title>
    <description>The latest articles on DEV Community by Virginia Nyambura  Mwega (@virginiamwega2svg).</description>
    <link>https://dev.to/virginiamwega2svg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4005611%2Faf72765f-e3ee-4aff-8c58-be369191fedb.jpeg</url>
      <title>DEV Community: Virginia Nyambura  Mwega</title>
      <link>https://dev.to/virginiamwega2svg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/virginiamwega2svg"/>
    <language>en</language>
    <item>
      <title>ktx Docs Review: Excellent for AI Agents, One Onboarding Gap</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Wed, 08 Jul 2026 16:29:42 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/ktx-docs-review-excellent-for-ai-agents-one-onboarding-gap-54ik</link>
      <guid>https://dev.to/virginiamwega2svg/ktx-docs-review-excellent-for-ai-agents-one-onboarding-gap-54ik</guid>
      <description>&lt;p&gt;A few days ago I announced a personal challenge: reviewing AI startup documentation in public — one thing done well, one place onboarding could be smoother, one rewritten example. This is review #1.&lt;/p&gt;

&lt;p&gt;The product: ktx by Kaelio — an open-source, self-improving context layer for data agents. It teaches coding agents like Claude Code, Codex, and Cursor how to query your warehouse accurately, using approved metric definitions and business context stored as reviewable YAML and Markdown in git. You install it with npm install -g @kaelio/ktx (Node.js 22+) and run one guided ktx setup.&lt;/p&gt;

&lt;p&gt;I tested the docs rather than just reading them. Everything below was verified live against docs.kaelio.com on July 8, 2026.&lt;/p&gt;

&lt;p&gt;What Kaelio does really well: docs written for agents, not just humans&lt;/p&gt;

&lt;p&gt;This is the part I want other AI startups to copy.&lt;/p&gt;

&lt;p&gt;Most documentation assumes a human reading a browser tab. ktx treats an AI coding agent as a first-class reader:&lt;/p&gt;

&lt;p&gt;A dedicated AI Resources route with an Agent Quickstart, Agent Instructions, and Prompt Recipes.&lt;br&gt;
/llms.txt (a curated index of high-value pages) and /llms-full.txt (the full corpus) so an assistant can discover the right pages before diving in.&lt;br&gt;
Per-page Markdown on demand: curl -H "Accept: text/markdown"  returns clean source — frontmatter stripped, code blocks preserved, tables preserved.&lt;br&gt;
Missing pages return a plain-text 404 instead of silently falling back to rendered HTML. Small detail, big kindness — it stops an agent from confidently parsing a garbage page.&lt;br&gt;
Copy as Markdown, View MD, and Copy MDX actions sit right on each rendered page.&lt;/p&gt;

&lt;p&gt;One of my standing review questions is literally: is the documentation structured so both developers and AI agents can use it effectively? ktx is the clearest "yes" I've come across. If you're building a developer product in 2026, this is the bar.&lt;/p&gt;

&lt;p&gt;The human path is strong too: a single guided ktx setup wizard with clearly numbered steps, a live demo warehouse with paste-ready credentials, and a Common issues table that maps symptom → fix.&lt;/p&gt;

&lt;p&gt;Where onboarding could be smoother: the MCP step the happy path skips&lt;/p&gt;

&lt;p&gt;Here's the one seam.&lt;/p&gt;

&lt;p&gt;The quickstart walks you through a clean sequence: install → ktx setup → ktx status (verify) → Connect a coding agent. That final section covers installing project-local agent rules with ktx setup --agents.&lt;/p&gt;

&lt;p&gt;What it doesn't mention is that, before your agent client can actually reach ktx, you may need to start the local MCP daemon. That step lives in the README and FAQ — not in the quickstart:&lt;/p&gt;

&lt;p&gt;The README notes: if ktx status prints ktx mcp start --project-dir ..., run it before opening your agent client.&lt;br&gt;
The FAQ explains there's no hosted service — the local MCP daemon runs on demand via ktx mcp start when an agent client needs it.&lt;/p&gt;

&lt;p&gt;So the failure mode is: a new developer follows the quickstart end-to-end, opens Claude Code or Cursor, and the agent silently can't see ktx — with no breadcrumb in the quickstart pointing at the fix. Making it slightly stickier, the ktx status example output in the Verify section shows a fully-ready project but doesn't include the ktx mcp start line the README says status can emit — so even the example doesn't prepare you for it.&lt;/p&gt;

&lt;p&gt;To be fair: ktx is under active development, and this is one missing signpost on an otherwise excellent path. But it sits at the last mile of onboarding — exactly where a new developer's first impression is decided.&lt;/p&gt;

&lt;p&gt;The rewritten section&lt;/p&gt;

&lt;p&gt;Here's how I'd close the gap — a version of "Connect a coding agent" written in ktx's own voice, folding in the missing step:&lt;/p&gt;

&lt;p&gt;Connect a coding agent&lt;/p&gt;

&lt;p&gt;The setup wizard installs project-local agent rules in its last step. To install or change targets later:&lt;/p&gt;

&lt;p&gt;ktx setup --agents&lt;/p&gt;

&lt;p&gt;Claude Code and Codex also support global installs with --global. Agent rules point at the ktx CLI path that created them, so agents don't need a separate ktx binary on PATH. If the CLI path changes, rerun ktx setup --agents.&lt;/p&gt;

&lt;p&gt;Start the MCP server if your status asks for it. Some setups serve agents through an on-demand local MCP daemon. If ktx status prints a line like:&lt;/p&gt;

&lt;p&gt;ktx mcp start --project-dir /home/user/analytics&lt;/p&gt;

&lt;p&gt;run that command before opening your agent client. Otherwise the agent connects to nothing and ktx tools appear empty. You can confirm with ktx status — Agent integration should read ready.&lt;/p&gt;

&lt;p&gt;One short subsection, one status breadcrumb. That's the whole fix — and it's the difference between a silent dead-end and a thirty-second recovery.&lt;/p&gt;

&lt;p&gt;The pattern worth noticing&lt;/p&gt;

&lt;p&gt;The irony here is instructive: ktx has some of the best agent-facing documentation I've reviewed, and its one gap is on the human path — a step the team likely stopped seeing because they already know it's there. That's the most common failure I find across audits: docs that are perfectly correct for someone who already understands the product, which is exactly the one audience that doesn't need them.&lt;/p&gt;

&lt;p&gt;If you write docs, the cheapest test available is still the best one: hand your quickstart to someone (or some agent) with zero context, and watch where they stall.&lt;/p&gt;

&lt;p&gt;Next review&lt;/p&gt;

&lt;p&gt;If you're building an AI product with public documentation, I'd love to include it — drop a link in the comments or reach out. Same format every time: one thing done well, one gap, one rewrite.&lt;/p&gt;

&lt;p&gt;I'm Virginia Mwega — a full-stack &amp;amp; AI engineer specializing in documentation engineering and developer experience. Portfolio: virginia-mwega.vercel.app · Writing: virginiamwega-com.vercel.app · Connect: &lt;a href="https://www.linkedin.com/in/virginia-mwega-196309313/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/virginia-mwega-196309313/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>ai</category>
      <category>devrel</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I Reviewed 10 AI Startup Documentation Sites. Here Are the 7 Mistakes I Kept Seeing.</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Mon, 06 Jul 2026 18:12:05 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/i-reviewed-10-ai-startup-documentation-sites-here-are-the-7-mistakes-i-kept-seeing-1j</link>
      <guid>https://dev.to/virginiamwega2svg/i-reviewed-10-ai-startup-documentation-sites-here-are-the-7-mistakes-i-kept-seeing-1j</guid>
      <description>&lt;p&gt;Documentation is often the first product a developer experiences.&lt;/p&gt;

&lt;p&gt;Before they see your architecture, your engineering culture, or your code quality, they interact with your documentation.&lt;/p&gt;

&lt;p&gt;If that experience is confusing, incomplete, or frustrating, many developers won't make it to their first successful API request.&lt;/p&gt;

&lt;p&gt;Over the past few weeks, I've been reviewing documentation from AI startups to understand what makes onboarding smooth—and where teams unintentionally create friction.&lt;/p&gt;

&lt;p&gt;While every company is different, the same patterns kept appearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Quickstarts assume too much
&lt;/h2&gt;

&lt;p&gt;Many Quickstarts jump straight into code without explaining prerequisites.&lt;/p&gt;

&lt;p&gt;Developers are expected to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where to get an API key&lt;/li&gt;
&lt;li&gt;Which SDK to install&lt;/li&gt;
&lt;li&gt;Required environment variables&lt;/li&gt;
&lt;li&gt;Authentication steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Quickstart should help someone go from zero to a successful request with as little guesswork as possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Error messages aren't documented
&lt;/h2&gt;

&lt;p&gt;Developers don't judge documentation by how it works when everything goes right.&lt;/p&gt;

&lt;p&gt;They judge it by how quickly it helps them recover when something goes wrong.&lt;/p&gt;

&lt;p&gt;Instead of only listing error codes, explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why the error happens&lt;/li&gt;
&lt;li&gt;Common causes&lt;/li&gt;
&lt;li&gt;How to fix it&lt;/li&gt;
&lt;li&gt;What to try next&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good troubleshooting documentation builds confidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Examples are incomplete
&lt;/h2&gt;

&lt;p&gt;Too many examples leave out important details.&lt;/p&gt;

&lt;p&gt;Developers shouldn't have to infer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication headers&lt;/li&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;Request payloads&lt;/li&gt;
&lt;li&gt;Expected responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples should be copy, paste, run, and understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. There's no clear learning path
&lt;/h2&gt;

&lt;p&gt;Documentation often feels like a collection of pages instead of a guided journey.&lt;/p&gt;

&lt;p&gt;A better structure might look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Quickstart&lt;/li&gt;
&lt;li&gt;Core Concepts&lt;/li&gt;
&lt;li&gt;Tutorials&lt;/li&gt;
&lt;li&gt;API Reference&lt;/li&gt;
&lt;li&gt;Advanced Guides&lt;/li&gt;
&lt;li&gt;Troubleshooting&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When developers always know what to read next, they make progress faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Documentation isn't written for AI-assisted development
&lt;/h2&gt;

&lt;p&gt;Today, developers increasingly rely on AI coding assistants.&lt;/p&gt;

&lt;p&gt;That means documentation should also be easy for AI tools to interpret.&lt;/p&gt;

&lt;p&gt;This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent headings&lt;/li&gt;
&lt;li&gt;Clear terminology&lt;/li&gt;
&lt;li&gt;Structured examples&lt;/li&gt;
&lt;li&gt;Explicit parameter descriptions&lt;/li&gt;
&lt;li&gt;Predictable page organization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Well-structured documentation helps both humans and AI systems retrieve accurate information.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Missing "next steps"
&lt;/h2&gt;

&lt;p&gt;A successful API call shouldn't be the end of the journey.&lt;/p&gt;

&lt;p&gt;Guide developers toward meaningful progress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build a chatbot&lt;/li&gt;
&lt;li&gt;Upload files&lt;/li&gt;
&lt;li&gt;Authenticate users&lt;/li&gt;
&lt;li&gt;Stream responses&lt;/li&gt;
&lt;li&gt;Explore advanced features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Momentum matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Documentation is treated as an afterthought
&lt;/h2&gt;

&lt;p&gt;The strongest engineering teams treat documentation as part of the product—not something that's written after the code ships.&lt;/p&gt;

&lt;p&gt;Documentation improves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developer experience&lt;/li&gt;
&lt;li&gt;Product adoption&lt;/li&gt;
&lt;li&gt;Support efficiency&lt;/li&gt;
&lt;li&gt;Customer success&lt;/li&gt;
&lt;li&gt;Developer trust&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not just a support resource.&lt;/p&gt;

&lt;p&gt;It's a growth asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Challenge
&lt;/h2&gt;

&lt;p&gt;I'm starting a public challenge where I review AI startup documentation and share practical improvements.&lt;/p&gt;

&lt;p&gt;Each review includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One thing the team did well&lt;/li&gt;
&lt;li&gt;One improvement opportunity&lt;/li&gt;
&lt;li&gt;A rewritten example or suggestion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to criticize.&lt;/p&gt;

&lt;p&gt;It's to learn, contribute, and help create better developer experiences.&lt;/p&gt;

&lt;p&gt;If you're building an AI product with public documentation, I'd love to review it.&lt;/p&gt;

&lt;p&gt;I'm always looking for examples of thoughtful documentation—and opportunities to make good docs even better.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>devrel</category>
      <category>documentation</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Documentation System Every Startup Should Have</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Wed, 01 Jul 2026 16:43:37 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/the-documentation-system-every-startup-should-have-139e</link>
      <guid>https://dev.to/virginiamwega2svg/the-documentation-system-every-startup-should-have-139e</guid>
      <description>&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most documentation advice is written by people whose job is &lt;em&gt;only&lt;/em&gt; documentation. This isn't that. This is the system I run as a solo founder shipping production AI, where docs that rot don't get caught by a docs team — they get caught by me, at the worst possible time.&lt;/li&gt;
&lt;li&gt;A documentation system isn't a wiki. It's four separable things (Diátaxis: tutorial, how-to, reference, explanation) plus a mechanism that keeps them from drifting out of sync with the code.&lt;/li&gt;
&lt;li&gt;The differentiator between docs that survive and docs that rot isn't writing quality. It's whether documentation is coupled to the same PR that changes the behavior, and whether it fails loudly when it goes stale.&lt;/li&gt;
&lt;li&gt;Reference docs should be &lt;em&gt;generated&lt;/em&gt; from typed code wherever possible, so the one category most likely to drift can't.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why you should ignore most documentation advice (including some of mine)
&lt;/h2&gt;

&lt;p&gt;Nearly every "how to do documentation" post is written by a technical writer — someone whose entire role is documentation, on a team where documentation is a budgeted function. That advice is often good, but it quietly assumes a luxury most early startups don't have: a person whose job is to notice when the docs stopped being true.&lt;/p&gt;

&lt;p&gt;I don't have that person. I'm a solo founder building a production AI system (FamNest, a multi-agent wellness coach). When my documentation drifts, no docs team catches it. I catch it — usually at 11pm, when I'm trying to remember how my own auth boundary works and the note I wrote three months ago is now confidently wrong.&lt;/p&gt;

&lt;p&gt;So the system I'm about to describe isn't optimized for a documentation department. It's optimized for the constraint most startups actually have: &lt;strong&gt;nobody's full-time job is keeping the docs honest, so the system itself has to do it.&lt;/strong&gt; That constraint changes the answer, and it's why this isn't a generic "write good docs" post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things a documentation system actually is
&lt;/h2&gt;

&lt;p&gt;The single most useful mental model I've found is Diátaxis — the framework created by Daniele Procida and now used by Cloudflare, Gatsby, and a chunk of the Python ecosystem. Its core claim is deceptively simple: there aren't many kinds of documentation, there are exactly &lt;strong&gt;four&lt;/strong&gt;, they serve different needs, and mixing them is the most common cause of docs that feel wrong even when every individual sentence is correct.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tutorials&lt;/strong&gt; — learning-oriented. A beginner, on rails, following exact steps to a known destination. Your quickstart is a tutorial. Its job is confidence, not completeness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How-to guides&lt;/strong&gt; — task-oriented. An already-competent user trying to accomplish a specific real-world goal. "How to configure X." Not teaching — helping someone who already knows the basics get a thing done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference&lt;/strong&gt; — information-oriented. The dry, factual, complete description of the machinery. Your API reference, your schema, your config options. Consulted, not read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt; — understanding-oriented. The "why" behind a design decision, read away from the keyboard. Why the architecture is shaped this way. This is the category startups skip and later regret skipping, because it's the institutional memory of &lt;em&gt;why&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason this matters practically: the most common documentation failure isn't a missing page, it's a &lt;strong&gt;tutorial that derailed into reference&lt;/strong&gt; — the getting-started guide that stops to enumerate every flag, until a beginner who wanted to feel capable instead feels buried. Once you can name the four types, you can see the mixing, and most "our docs are confusing" problems turn out to be mixing problems, not writing problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody frames as engineering: keeping it from rotting
&lt;/h2&gt;

&lt;p&gt;Here's the thing the framework alone won't save you from, and where the solo-builder constraint actually produces a &lt;em&gt;better&lt;/em&gt; answer than the well-resourced one.&lt;/p&gt;

&lt;p&gt;Documentation doesn't fail at the moment of writing. It fails silently, later, every time the code changes and the sentence describing it doesn't. A page that's 80% accurate is arguably worse than a missing page — a missing page sends you to read the source; a subtly-wrong page confidently teaches you the wrong thing and costs you an hour before you stop trusting it.&lt;/p&gt;

&lt;p&gt;The teams that beat this don't beat it with discipline. Discipline doesn't scale and it definitely doesn't survive a solo founder's worst week. They beat it structurally, by borrowing the property that makes tests valuable: &lt;strong&gt;failing loudly instead of silently.&lt;/strong&gt; Concretely, the system I run has three rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Docs live in the same PR as the behavior they describe.&lt;/strong&gt; If a change to an endpoint doesn't touch the docs, that's a visible gap in the diff, not an invisible one discovered months later. This is the single highest-leverage practice and it costs nothing but a PR-template checkbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Reference is generated, not hand-written, wherever the code is typed.&lt;/strong&gt; Reference is the category most prone to drift — it's the most detailed and the most tightly coupled to code. So it's the category you should hand-write the &lt;em&gt;least&lt;/em&gt;. My Next.js API routes are typed; the OpenAPI reference is generated from those types, which means the one kind of documentation most likely to lie can't, because it has no independent existence to drift away from. &lt;em&gt;(I've got a full walkthrough of the endpoint-to-OpenAPI generation as its own post — linked at the end.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Some docs should be executable, so staleness breaks a build.&lt;/strong&gt; A code example that's just text will rot. A code example that actually runs in CI fails the day it stops being true. The closer your documentation sits to something that executes, the less it can quietly lie to you. My agent-boundary contracts are validated with Zod, which means the "shape" documentation and the runtime enforcement are the same artifact — the docs can't disagree with the code because they &lt;em&gt;are&lt;/em&gt; the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The startup-specific version of this
&lt;/h2&gt;

&lt;p&gt;If you're pre-first-hire, here's the minimum viable documentation system, in priority order — not "write all four types of everything," which is how docs projects die, but the smallest thing that compounds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One tutorial&lt;/strong&gt; — a quickstart that actually works if a stranger copy-pastes it. Test it by having someone who isn't you run it. If they get stuck, that's a bug, not their fault.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated reference&lt;/strong&gt; — wire your typed API to auto-generated reference so the most drift-prone category maintains itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A running "why" doc&lt;/strong&gt; — one explanation file where you write down &lt;em&gt;why&lt;/em&gt; you made the load-bearing decisions, as you make them. This is the cheapest thing on the list and the one you'll be most grateful for in a year, because it's the context you can't reconstruct later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How-to guides on demand&lt;/strong&gt; — write one every time you answer the same question twice. Let real friction, not a content plan, decide what gets written.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice what's not on the list: exhaustive coverage. A documentation system isn't measured by how many pages it has. It's measured by whether the pages it has stay true, and whether the structure tells a reader which kind of page they're looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm the one writing this
&lt;/h2&gt;

&lt;p&gt;Because I live on the wrong side of the constraint that makes this advice real. I don't document my systems because a style guide told me to — I document them because I'm the one who has to reload the entire context of my own architecture at unpredictable moments, alone, and the difference between a system I documented well and one I didn't is whether that reload takes five minutes or two hours.&lt;/p&gt;

&lt;p&gt;That's also, not coincidentally, exactly the position a &lt;em&gt;new engineer joining your startup&lt;/em&gt; is in. Every argument I just made for why I document my own system for my own future self is the same argument for why your docs determine how fast your next hire becomes useful. The solo constraint just makes the cost legible sooner, because the person paying it is me, tonight.&lt;/p&gt;

&lt;p&gt;A documentation system every startup should have, in one sentence: &lt;strong&gt;four clearly separated types, reference generated from typed code, docs coupled to the PRs that change behavior, and the "why" written down as you go.&lt;/strong&gt; Everything else is a variation on those four moves.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build FamNest and write about the engineering underneath it. The specific pieces of the system above have their own deep-dives — endpoint-to-OpenAPI generation, validating agent boundaries with Zod, and treating Supabase RLS as living auth documentation — linked as I publish them in this series.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>devrel</category>
      <category>startup</category>
      <category>api</category>
    </item>
    <item>
      <title>Evaluating Agents With an LLM-as-Judge Harness (Without Kidding Yourself About It)</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Wed, 01 Jul 2026 15:54:18 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/evaluating-agents-with-an-llm-as-judge-harness-without-kidding-yourself-about-it-186k</link>
      <guid>https://dev.to/virginiamwega2svg/evaluating-agents-with-an-llm-as-judge-harness-without-kidding-yourself-about-it-186k</guid>
      <description>&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can't unit-test a coach agent the way you test a pure function — the output is non-deterministic and "good" is a judgment call, not an assertion.&lt;/li&gt;
&lt;li&gt;An LLM-as-judge harness lets you grade a whole test set automatically against a rubric, which is the only way solo-scale eval stays sustainable.&lt;/li&gt;
&lt;li&gt;But the judge is itself a fallible model. If you don't design around its known biases — position, verbosity, self-preference, and quiet drift when the judge model updates — you build a green dashboard that means nothing.&lt;/li&gt;
&lt;li&gt;The mitigations that actually work are mechanical, not prompt-magic: shuffle order on every pairwise call, pin the judge version, keep a small human-labelled anchor set, and re-check the judge against it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem I actually had
&lt;/h2&gt;

&lt;p&gt;FamNest's coach agent generates responses to parents — check-ins, encouragement, the occasional gentle redirect. I have a growing pile of these interactions, and every time I change a prompt, swap a model, or adjust the pipeline, I need to know one thing: &lt;strong&gt;did I just make it better or worse?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For normal code, that's what tests are for. I change something, the suite runs, red or green, done. But there's no &lt;code&gt;assertEqual&lt;/code&gt; for "was this an empathetic, useful response to a tired parent." The output changes every run even at temperature zero-ish, and the quality bar is a human judgment, not a fixed string. Two responses can be worded completely differently and both be good. One can match my "expected output" word for word and still be worse than a version that didn't.&lt;/p&gt;

&lt;p&gt;So the honest options were: read every response by hand every time I change something (does not scale past about week two), or build a harness where a model grades the outputs against a rubric. I built the harness. Then I spent an uncomfortable amount of time learning all the ways a harness like that can lie to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the harness actually is
&lt;/h2&gt;

&lt;p&gt;At its simplest, it's a loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_cases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coach_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;test_cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coach_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;rubric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COACH_RUBRIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;case_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;COACH_RUBRIC&lt;/code&gt; is the part that matters. It's not "rate this 1–10" — that produces mush. It's specific, scored dimensions: does the response acknowledge the actual thing the user said (not a generic version of it)? Does it avoid giving medical advice? Is it the right length for the moment, or is it a wall of text at someone who's exhausted? Each dimension gets a small integer and a one-line justification, and the harness keeps the justification, not just the number — because when the score drops, the reasoning is what tells me whether the agent regressed or the judge just had an opinion.&lt;/p&gt;

&lt;p&gt;That last distinction is the whole game.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where I stopped trusting the judge
&lt;/h2&gt;

&lt;p&gt;Here's the failure mode that made me rebuild the whole thing. You score helpfulness at 0.91 all quarter. Then the judge model ships a minor version bump. The mean shifts a few points, the distribution narrows, and your CI gate keeps passing — so you don't look. Weeks later the agent does something genuinely bad and the eval never flagged it, because the judge changed underneath you and the number stopped meaning what it meant the day you set the threshold.&lt;/p&gt;

&lt;p&gt;The research here is not subtle, and it's worth internalizing before you trust a single green checkmark. A 2026 RAND study that stress-tested judges across multiple benchmarks concluded that no judge was uniformly reliable, and frontier models exceeded 50% error rates on hard bias benchmarks. Consistency broke on inputs as trivial as formatting changes and paraphrasing. Separately, the classic MT-Bench work found that in pairwise comparisons, the answer in the first slot wins something like 10–15 points more often &lt;em&gt;purely because it's first&lt;/em&gt; — position bias, nothing to do with quality.&lt;/p&gt;

&lt;p&gt;(Worth noting the field isn't static: some 2026 reproductions find position bias has shrunk to near-negligible on current-gen models under a clean pairwise rubric, while verbosity bias stays small. Which is exactly the point — the biases move as the models move, so you measure them yourself rather than trusting a blog post from last year, including this one.)&lt;/p&gt;

&lt;p&gt;The named biases I actually design around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Position bias&lt;/strong&gt; — in any A-vs-B comparison, slot order can decide the winner. Mitigation: run every pairwise comparison twice with the order flipped, and only count it if the verdict is stable across both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verbosity bias&lt;/strong&gt; — longer answers tend to score higher even at matched quality. Mitigation: put length appropriateness &lt;em&gt;in the rubric as an explicit dimension&lt;/em&gt; so the judge is scoring it on purpose instead of rewarding it by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-preference&lt;/strong&gt; — a judge from the same model family as the candidate tends to over-score it. Mitigation: don't let the judge be the same model as the agent it's grading. (In my case the coach runs on one provider; I judge with a different family entirely.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibration drift&lt;/strong&gt; — the silent one above. Mitigation below, because it's the most important.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The anchor set is the thing that keeps you honest
&lt;/h2&gt;

&lt;p&gt;The single highest-leverage piece of the harness isn't the judge prompt. It's a small set — a few dozen cases — that I labelled by hand, carefully, once. Good responses, bad responses, and the genuinely ambiguous ones. That's my ground truth.&lt;/p&gt;

&lt;p&gt;Every time I run the harness, it grades the anchor set too. If the judge's scores on those known cases still line up with my human labels, I trust its scores on the rest of the run. If the judge drifts on the anchor set — because the model updated, because I tweaked the rubric, because Mercury is in retrograde — I find out immediately, on cases where I already know the right answer, instead of finding out in production on a case where I don't.&lt;/p&gt;

&lt;p&gt;This is the same instinct as the deterministic crisis floor I wrote about earlier in this series: &lt;strong&gt;the most consequential check should be the one that's simplest and least dependent on a model behaving.&lt;/strong&gt; For safety, that's regex. For evaluation, it's a few dozen examples I graded with my own eyes and refuse to let a model overrule silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting this
&lt;/h2&gt;

&lt;p&gt;Build the harness — reading every output by hand does not scale, and an LLM judge genuinely does correlate with human preference well enough to be useful. But treat the judge as a component that can fail, not an oracle. Pin its version so it doesn't change without you deciding. Shuffle order on comparisons. Keep the reasoning, not just the score. And keep a small hand-labelled anchor set that you re-check every single run, because a green eval dashboard that you never validate is worse than no dashboard — it's the &lt;em&gt;confidence&lt;/em&gt; of measurement without the substance, and that's exactly the kind of thing that ships a broken agent with a clean conscience.&lt;/p&gt;

&lt;p&gt;The harness didn't remove my judgment from the loop. It moved my judgment to where it's cheap and permanent — a small set of examples I curate once — instead of where it's expensive and forgettable, which is re-reading a hundred responses every time I touch a prompt.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of an ongoing series documenting FamNest's architecture. Earlier posts cover the deterministic crisis floor and the multi-agent coach pipeline. Next: how we test a non-deterministic system end to end.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>systemdesign</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The doc was the spec: building a safety layer for an AI app that talks to tired parents published: false</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Tue, 30 Jun 2026 10:23:52 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/the-doc-was-the-spec-building-a-safety-layer-for-an-ai-app-that-talks-to-tired-parents-published-1pnd</link>
      <guid>https://dev.to/virginiamwega2svg/the-doc-was-the-spec-building-a-safety-layer-for-an-ai-app-that-talks-to-tired-parents-published-1pnd</guid>
      <description>&lt;p&gt;"&lt;em&gt;What happened when I stopped designing my AI coach for flexibility and started designing it to be predictable enough to write down."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last month I deleted three features from my AI app. Nobody asked me to.&lt;br&gt;
I removed them because I couldn't explain them clearly. And I've started treating that as a hard signal: if I can't describe how a feature behaves in plain language a tired parent would understand, it isn't finished — it's a liability.&lt;br&gt;
This post is about the engineering that came out of that rule. It's specific, it has code, and it's the part of building an AI product that nobody puts in the demo.&lt;br&gt;
The context&lt;br&gt;
I'm building FamNest, an AI wellness tool for parents. The user I design for is the one refreshing a feeding tracker at 3am, googling "is this normal" with one hand while holding a sleeping baby with the other.&lt;br&gt;
That user changes your engineering priorities. Latency matters less than predictability. "Clever" is a risk, not a feature. And the moment your AI says something subtly wrong to someone in that state, you've lost them — and possibly done real harm.&lt;br&gt;
So the question I kept returning to wasn't "what can the model do?" It was "what is the model never allowed to improvise?"&lt;br&gt;
Problem 1: a coach with too many answers&lt;br&gt;
In the first version, my AI coach could respond to the same question a dozen slightly different ways. It felt smart. Flexible, even.&lt;br&gt;
It was actually a nightmare. When I sat down to write the documentation, I couldn't promise a parent what the system would do. And if I can't promise it, why should they trust it?&lt;br&gt;
The fix was to put a second model in front of the user — a safety reviewer — and give it exactly three outcomes. Not a confidence score. Not a freeform critique. Three discrete verdicts.&lt;br&gt;
``type Verdict = "ok" | "revise" | "crisis";&lt;/p&gt;

&lt;p&gt;interface ReviewResult {&lt;br&gt;
  verdict: Verdict;&lt;br&gt;
  reason: string;        // for logging, never shown to the user&lt;br&gt;
  revisedDraft?: string; // only present when verdict === "revise"&lt;br&gt;
}``&lt;br&gt;
The coach generates a draft. The reviewer judges it. The whole contract fits in four lines, and that's the point — I can document it in one sentence: the reviewer either approves the draft, rewrites it, or escalates to a crisis response.&lt;/p&gt;

&lt;p&gt;``async function generateReply(userMessage: string): Promise {&lt;br&gt;
  const draft = await coach.respond(userMessage);&lt;br&gt;
  const review = await safetyReviewer.review(userMessage, draft);&lt;/p&gt;

&lt;p&gt;switch (review.verdict) {&lt;br&gt;
    case "ok":&lt;br&gt;
      return draft;&lt;br&gt;
    case "revise":&lt;br&gt;
      return review.revisedDraft ?? draft;&lt;br&gt;
    case "crisis":&lt;br&gt;
      return CRISIS_RESPONSE; // see below — this is not generated&lt;br&gt;
  }&lt;br&gt;
}&lt;code&gt;&lt;br&gt;
Problem 2: the crisis floor&lt;br&gt;
Here's the decision I'm most sure about.&lt;br&gt;
When a message hints at a crisis, the system does not generate a response. It returns fixed, human-reviewed text. Every time. Byte for byte.&lt;br&gt;
&lt;/code&gt;// This string is reviewed by a human, version-controlled,&lt;br&gt;
// and documented word-for-word in the user-facing docs.&lt;br&gt;
const CRISIS_RESPONSE = &lt;code&gt;&lt;br&gt;
It sounds like you're going through something really hard right now.&lt;br&gt;
You don't have to handle this alone. If you're in immediate danger,&lt;br&gt;
please contact your local emergency number...&lt;br&gt;
&lt;/code&gt;.trim();&lt;code&gt;&lt;br&gt;
This is the opposite of how we usually think about generative AI. The whole appeal of an LLM is that it composes something new. But the situation where a parent most needs the response to be right is exactly the situation where I least want the model to be creative.&lt;br&gt;
I call this the crisis floor: a deterministic baseline that the system can never generate its way below. The model can make the experience better above the floor. It is never allowed to touch what happens at it.&lt;br&gt;
A subtle but important detail: the floor is checked before the clever path, not after.&lt;br&gt;
&lt;/code&gt;async function handleMessage(userMessage: string): Promise {&lt;br&gt;
  // Deterministic guardrail runs first, independent of the LLM.&lt;br&gt;
  if (crisisFloor.matches(userMessage)) {&lt;br&gt;
    return CRISIS_RESPONSE;&lt;br&gt;
  }&lt;br&gt;
  return generateReply(userMessage);&lt;br&gt;
}&lt;code&gt;&lt;br&gt;
If the generative pipeline is down, malfunctioning, or hallucinating, the floor still holds. It doesn't depend on the part of the system most likely to fail.&lt;br&gt;
Problem 3: what happens when the provider falls over&lt;br&gt;
LLM APIs go down. Rate limits hit. A region has a bad day. For most apps that's an annoyance. For an app a parent leans on at 3am, a blank screen is a broken promise.&lt;br&gt;
So every external dependency has a known fallback, and "the model is unavailable" is a documented state — not an exception that bubbles up to a stack trace.&lt;br&gt;
&lt;/code&gt;async function coachRespond(message: string): Promise {&lt;br&gt;
  try {&lt;br&gt;
    return await llm.complete(buildPrompt(message));&lt;br&gt;
  } catch (err) {&lt;br&gt;
    logger.warn("LLM provider unavailable, degrading gracefully", { err });&lt;br&gt;
    // A safe, generic, pre-written reply. Not an error.&lt;br&gt;
    return GRACEFUL_FALLBACK;&lt;br&gt;
  }&lt;br&gt;
}``&lt;br&gt;
The user gets a calm, honest message instead of a spinner that never resolves. Graceful degradation isn't a nice-to-have here. It's part of the trust contract.&lt;br&gt;
The thing I actually learned&lt;br&gt;
The pattern underneath all three: documentation wasn't something I wrote after the engineering. It was the engineering.&lt;br&gt;
The doc became the spec. When a behavior couldn't be written down cleanly — three verdicts, fixed crisis text, a named fallback state — that was the signal the design was wrong, not the writing.&lt;br&gt;
It turns out "predictable enough to document" is a great design constraint. It pushes you toward small, discrete contracts and away from the kind of open-ended cleverness that demos well and ships badly.&lt;br&gt;
I think a lot of AI products are shipping behavior they can't fully explain and quietly calling that confusion "intelligence." For the users I build for, clarity is the feature.&lt;br&gt;
If you can't document it, you don't understand it yet.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I write about building and documenting production AI systems. If you've shipped a guardrail or fallback you're proud of — or one that bit you — I'd genuinely like to hear about it in .&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Writing API docs an AI agent can actually consume</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Mon, 29 Jun 2026 15:01:58 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/writing-api-docs-an-ai-agent-can-actually-consume-16bb</link>
      <guid>https://dev.to/virginiamwega2svg/writing-api-docs-an-ai-agent-can-actually-consume-16bb</guid>
      <description>&lt;p&gt;&lt;em&gt;Your docs are written for a human who can guess. The agent calling your API can't&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I found the gap the embarrassing way: one of my own agents couldn't call one of my own APIs.&lt;/p&gt;

&lt;p&gt;FamNest runs a small agent graph — a router hands a parent's message to a retriever, the retriever grounds an answer in a vetted corpus, a coach agent (Groq, Llama 3.3 70B) drafts a reply, and a safety-reviewer agent signs off before anything reaches a human. The agents call internal endpoints the same way a third-party integrator would. And one afternoon the coach kept constructing malformed calls to the retrieval endpoint — wrong field name, missing a required filter, occasionally inventing a parameter that never existed.&lt;/p&gt;

&lt;p&gt;The endpoint wasn't broken. The docs were. They were written for a human who could fill in the blanks, and the agent had no blanks to fill — only the tokens I gave it.&lt;/p&gt;

&lt;p&gt;That's the whole lesson, and it's worth more than a trend.&lt;/p&gt;

&lt;p&gt;The trend everyone's shipping — and where it stops&lt;/p&gt;

&lt;p&gt;If you've touched developer tooling in 2026 you've watched llms.txt go from a September-2024 proposal to a routine piece of infrastructure. It's a Markdown file at your domain root that points AI systems at the content that matters, with a one-line summary of each link. Mintlify, Fern, and GitBook ship one-click toggles for it. IDE agents — Cursor, Windsurf, Claude Code, Copilot — fetch it when you point them at a docs site, then pull only the linked pages they need before writing code. LangChain even shipped an MCP server (mcpdoc) that hands those files to host apps as a fetch_docs tool.&lt;/p&gt;

&lt;p&gt;People are calling this the Business-to-Agent web, and the framing is right: just as you once needed a site humans could navigate, you now need surfaces agents can route on. Ship the llms.txt. It's a half-day of work.&lt;/p&gt;

&lt;p&gt;But notice what it actually solves: discovery. It answers "which page matters." It says nothing about the harder question that broke my coach agent:&lt;/p&gt;

&lt;p&gt;Once the agent has found your endpoint, can it call it correctly on the first try — with no human in the loop to recover when your prose is ambiguous?&lt;/p&gt;

&lt;p&gt;That's not a discovery problem. That's a contract problem. And it's where most docs quietly fail.&lt;/p&gt;

&lt;p&gt;An agent is a different kind of reader&lt;/p&gt;

&lt;p&gt;A human reading your docs brings a lifetime of priors. They infer that userId is probably a UUID. They notice the example uses snake_case and adjust. They hit a 400, shrug, read the error, and try again. If they're really stuck they ask a teammate. Human docs can be good enough because the human closes the gap.&lt;/p&gt;

&lt;p&gt;An agent closes nothing. It has your tokens and a probability distribution. It pattern-matches structure: if your example shows one field, it produces one field; if you describe an error in a sentence, it treats the sentence as flavor, not as a branch it has to handle. Ambiguity doesn't make an agent cautious — it makes it confident and wrong.&lt;/p&gt;

&lt;p&gt;So the doc stops being documentation and becomes the interface itself. Everything the agent will ever know about your endpoint is in the text. If a fact isn't on the page, it doesn't exist.&lt;/p&gt;

&lt;p&gt;That reframes what a good endpoint doc has to contain. Here are the five things mine were missing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A typed schema, not a prose description&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Prose says: "Send the user's question and an optional list of topic tags."&lt;/p&gt;

&lt;p&gt;A schema says exactly what's allowed, and an agent can pattern-match it without guessing:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ts// retrieve — request&lt;br&gt;
const RetrieveRequest = z.object({&lt;br&gt;
  query: z.string().min(1).max(2000),&lt;br&gt;
  topics: z.array(z.enum(["sleep", "feeding", "behavior", "self_care"]))&lt;br&gt;
            .max(4)&lt;br&gt;
            .default([]),&lt;br&gt;
  topK: z.number().int().min(1).max(10).default(5),&lt;br&gt;
});&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The difference is the enum, the bounds, the default. "Optional list of topic tags" let my coach invent "toddler_tantrums". z.enum([...]) makes the valid set unguessable-wrong. Publish the schema, not a paragraph about the schema.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Exhaustive examples — including the unhappy paths&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Agents copy examples. Whatever you show is what you'll get back. If your only example is the happy path, the happy path is the only thing the model knows how to produce.&lt;/p&gt;

&lt;p&gt;So I document the empty result and the rejected request as first-class examples, not footnotes:&lt;/p&gt;

&lt;p&gt;``jsonc// 200 — results found&lt;br&gt;
{ "matches": [{ "id": "c_18", "score": 0.82, "text": "..." }], "truncated": false }&lt;/p&gt;

&lt;p&gt;// 200 — valid query, nothing relevant (NOT an error)&lt;br&gt;
{ "matches": [], "truncated": false }&lt;/p&gt;

&lt;p&gt;// 422 — query failed validation&lt;br&gt;
{ "error": "validation_error", "field": "topics", "detail": "unknown topic 'toddler_tantrums'" }`&lt;/p&gt;

&lt;p&gt;The middle case is the one humans leave out and agents desperately need. "No matches" is a normal outcome, not a failure — and if you don't say so, the agent will treat an empty array as a bug and retry forever.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An error taxonomy with recovery semantics&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most docs describe errors. Agents need to be told what to do about them. "Returns 429 when rate-limited" is a description. An agent needs a decision.&lt;/p&gt;

&lt;p&gt;So I ship a table where every row ends in an action:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;&lt;code&gt;error&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;What the caller should do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;422&lt;/td&gt;
&lt;td&gt;&lt;code&gt;validation_error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bad input&lt;/td&gt;
&lt;td&gt;Fix the field named in &lt;code&gt;detail&lt;/code&gt;; do &lt;strong&gt;not&lt;/strong&gt; retry unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rate_limited&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Too many calls&lt;/td&gt;
&lt;td&gt;Back off using &lt;code&gt;Retry-After&lt;/code&gt;; retry the same body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;td&gt;&lt;code&gt;model_unavailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Upstream LLM down&lt;/td&gt;
&lt;td&gt;Fall back to cached/deterministic path; do not retry tightly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;td&gt;&lt;code&gt;idempotency_conflict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key reused, different body&lt;/td&gt;
&lt;td&gt;Stop; surface to a human&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A human reads that table for reference. An agent reads it as a control-flow graph. The "do not retry" cells are the ones that stop a confused agent from hammering your endpoint at 3am.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An explicit determinism / idempotency contract&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The single most useful sentence I added to any endpoint doc was: "Is it safe to retry this?"&lt;/p&gt;

&lt;p&gt;Agents retry. Networks are flaky, and a retried call that isn't idempotent is how you double-charge a card or send two replies to one anxious parent. For anything with a side effect, I now state the contract in the doc itself:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
Idempotency: required for POST /coach/reply and all payment routes.&lt;br&gt;
Send an `Idempotency-Key` header (UUID). Replays with the same key + same&lt;br&gt;
body return the original result. Same key + different body → 409.&lt;br&gt;
Retrieval (GET /retrieve) is side-effect-free and safe to retry freely.&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That paragraph is the difference between a retry loop that heals and one that does damage. It's also the kind of thing humans infer and agents simply won't — there is no prior that tells a model your payment webhook is replay-safe. You have to say it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Auth and limits as data, not folklore&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"Authenticated requests only, please don't spam it" is not a contract. Scopes, the exact header, the rate limit, and the window belong in the doc as structured fields the agent can read and self-regulate against:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;plaintext&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Auth: Bearer token in&lt;/code&gt;Authorization&lt;code&gt;. Scope&lt;/code&gt;coach:read&lt;code&gt;for /retrieve.&lt;br&gt;
Limits: 60 req/min/token. On exceed → 429 +&lt;/code&gt;Retry-After` (seconds).&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;plaintext&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;Now the agent can pace itself instead of discovering your limit by tripping it.&lt;/p&gt;

&lt;p&gt;Keep it honest: one source of truth&lt;/p&gt;

&lt;p&gt;All of this rots the moment your docs and your code disagree — and an agent can't smell a stale doc the way a human can. So the contract has to be generated, not hand-maintained.&lt;/p&gt;

&lt;p&gt;My chain is boring on purpose: the typed Next.js handler validates with the Zod schema, the schema generates the OpenAPI spec, and my llms.txt links to the generated reference. The schema is the only thing I edit. The doc can't drift, because the doc is downstream of the thing that's actually true.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Zod schema  ──►  request validation (runtime)&lt;br&gt;
     │&lt;br&gt;
     └────────►  OpenAPI spec  ──►  /llms.txt entry  ──►  agent reads it&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
If the handler changes, every artifact downstream changes with it. The doc lies only if the code lies.&lt;/p&gt;

&lt;p&gt;The test that actually proves it&lt;/p&gt;

&lt;p&gt;Here's the check I run before I trust an endpoint doc: give a fresh model only the doc — no codebase, no context — and ask it to (a) construct a valid call and (b) handle a seeded error. If it can't, the gap is in the doc, not the model. I keep these as tiny snapshot tests next to the endpoint, so a doc regression fails CI like any other bug.&lt;/p&gt;

&lt;p&gt;When my coach agent broke, this test would have caught it in seconds. The retrieval doc, fed to a cold model, produced exactly the malformed call I saw in production — because the ambiguity was right there on the page.&lt;/p&gt;

&lt;p&gt;This isn't new. It's just a new reader.&lt;/p&gt;

&lt;p&gt;None of this is novel discipline. "Unambiguous, complete, verifiable" is the spine of IEEE 29148 — the requirements standard I write everything against. What's changed is that the consumer of your interface is no longer guaranteed to be a person who can paper over a vague spec. Half your integrators in 2026 are agents, and they read your docs literally, exhaustively, and without charity.&lt;/p&gt;

&lt;p&gt;Ship the llms.txt so agents can find you. But the thing that makes them succeed once they arrive is older and less glamorous: a contract precise enough that it can't be misread. The agentic web doesn't need prettier docs. It needs docs that can't be guessed wrong.&lt;/p&gt;

&lt;p&gt;I build FamNest, an AI wellness coach for busy parents, and write about production reliability and safety for multi-agent systems. If you're documenting an agent-callable API and want a second pair of eyes on the contract, my notes are open.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>documentation</category>
      <category>webdev</category>
      <category>llm</category>
    </item>
    <item>
      <title>Your WHERE clause is not a security boundary (multi-tenant RAG with pgvector + RLS)</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Sun, 28 Jun 2026 19:01:51 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/your-where-clause-is-not-a-security-boundary-multi-tenant-rag-with-pgvector-rls-28fk</link>
      <guid>https://dev.to/virginiamwega2svg/your-where-clause-is-not-a-security-boundary-multi-tenant-rag-with-pgvector-rls-28fk</guid>
      <description>&lt;p&gt;&lt;em&gt;TL;DR: app-layer filtering is a single point of failure. Push tenant isolation into Postgres with RLS — and watch out for the security definer trap in your vector-search function.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your WHERE clause is not a security boundary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My app is an AI wellness coach for parents. Every user's data is about the most private thing they have: how they're actually coping. Their check-ins, their bad nights, the things they'd never say out loud. The whole product runs on retrieval — when someone talks to the coach, the system pulls their relevant history out of a vector store and grounds the response in it.&lt;/p&gt;

&lt;p&gt;Which means the single scariest bug I can imagine isn't a crash. It's user A asking a question and the retrieval quietly returning a snippet of user B's private history. No error. No stack trace. Just one person's worst night surfacing in another person's conversation.&lt;/p&gt;

&lt;p&gt;In a multi-tenant app, that bug is one forgotten line of code away at all times. Here's how I make sure it can't happen — and the part of it that no tutorial warns you about.&lt;/p&gt;

&lt;p&gt;The obvious fix is a single point of failure&lt;/p&gt;

&lt;p&gt;The instinctive way to keep tenants apart is to filter in your query:&lt;/p&gt;

&lt;p&gt;``&lt;br&gt;
sql&lt;br&gt;
select * from embeddings&lt;br&gt;
where user_id = $current_user&lt;br&gt;
order by embedding &amp;lt;=&amp;gt; $query&lt;br&gt;
limit 5;&lt;/p&gt;

&lt;p&gt;``&lt;/p&gt;

&lt;p&gt;This works. It also relies on me, a tired human, remembering to write where user_id = ... on every single query that ever touches that table, forever, across every feature, including the ones I haven't built yet.&lt;/p&gt;

&lt;p&gt;That's not a security boundary. That's a promise. And the failure mode of a promise is that the day you forget it — or a new query path skips it, or a refactor drops it  there is nothing underneath to catch you. The app returns the wrong tenant's data and looks completely healthy doing it. That's exactly the shape of bug I caught in my own audit once. I didn't want to rely on never making it again.&lt;/p&gt;

&lt;p&gt;Isolation belongs in the database, not the application&lt;/p&gt;

&lt;p&gt;The fix is to move the boundary down a layer, into Postgres itself, using Row Level Security. RLS lets the database enforce which rows a user is even allowed to see, regardless of what the query asks for.&lt;/p&gt;

&lt;p&gt;``&lt;br&gt;
sql&lt;br&gt;
alter table embeddings enable row level security;&lt;/p&gt;

&lt;p&gt;create policy "Users read their own embeddings"&lt;br&gt;
on embeddings for select&lt;br&gt;
using (auth.uid() = user_id);&lt;/p&gt;

&lt;p&gt;``&lt;/p&gt;

&lt;p&gt;Now the rule isn't "please remember to filter." The rule is: this user physically cannot select another user's rows, because the database won't return them. A query that forgets the filter still comes back isolated, because the isolation isn't in the query anymore — it's in the table.&lt;/p&gt;

&lt;p&gt;This is defense in depth, the same principle security people have leaned on for decades. The app-layer filter is still there as the first line. RLS is the backstop that makes a mistake in that first line survivable instead of catastrophic. One layer can fail without the whole guarantee failing.&lt;/p&gt;

&lt;p&gt;The pgvector trap nobody mentions&lt;/p&gt;

&lt;p&gt;Here's where it gets interesting, and where I'd put real money that most "build RAG on Supabase" tutorials are quietly broken.&lt;/p&gt;

&lt;p&gt;Vector similarity search is usually wrapped in a SQL function — a match_documents-style RPC  so you can call it cleanly from your app and keep the ANN index happy:&lt;/p&gt;

&lt;p&gt;``&lt;br&gt;
sql&lt;br&gt;
create function match_user_docs(query_embedding vector(1536), match_count int)&lt;br&gt;
returns setof embeddings&lt;br&gt;
language sql&lt;br&gt;
as $$&lt;br&gt;
  select *&lt;br&gt;
  from embeddings&lt;br&gt;
  order by embedding &amp;lt;=&amp;gt; query_embedding&lt;br&gt;
  limit match_count;&lt;br&gt;
$$;&lt;/p&gt;

&lt;p&gt;``&lt;/p&gt;

&lt;p&gt;The footgun is the function's security mode. If you mark a function security definer — and a lot of copy-pasted vector-search examples do, to smooth over permissions — it runs with the definer's privileges and bypasses the caller's RLS entirely. You carefully set up Row Level Security on the table, then call it through a function that turns that protection off, and you'd never know: the function returns results, the app works in the demo, and every tenant's vectors are quietly reachable through that one call.&lt;/p&gt;

&lt;p&gt;The fix is boring and important: keep the search function security invoker so the caller's RLS still applies, or — if it genuinely has to be security definer — filter by auth.uid() inside the function and pin the search_path. The point is to never let the convenience wrapper become the hole in the wall you just built.&lt;/p&gt;

&lt;p&gt;One more wrinkle: filtering and approximate search fight a little&lt;/p&gt;

&lt;p&gt;There's a subtle performance interaction worth knowing. pgvector's index (HNSW or IVFFlat) does approximate nearest-neighbor search — it returns roughly the closest vectors, fast. Add RLS on top, and the isolation filter trims that candidate set down to the current tenant's rows.&lt;/p&gt;

&lt;p&gt;If you ask the index for the global top 5 and then isolation removes the ones that aren't yours, you can end up with fewer than 5 results — or, in a busy table, none. The pattern is to over-fetch: ask the index for more candidates than you need, so that after isolation you still have enough to ground a good answer. It's a small thing that only shows up under real multi-tenant load, which is exactly why it's worth saying out loud.&lt;/p&gt;

&lt;p&gt;The takeaway&lt;/p&gt;

&lt;p&gt;The model gets all the attention, but the part of an AI app that has to be certain is rarely the model. Here, it's the data boundary. And a boundary you enforce in application code is only as strong as your memory on your worst day.&lt;/p&gt;

&lt;p&gt;So I push it down to where it can't be forgotten. The app filters because it should. The database isolates because it must. One forgotten where clause should be a non-event, not a breach — and the only way to guarantee that is to stop trusting the query and start trusting the table.&lt;/p&gt;

</description>
      <category>database</category>
      <category>ai</category>
      <category>security</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
