Documentation That Survives
Stage 3 · Testing, Quality & Craft · B.U.I.L.D. letter: D
You shipped the feature. You were proud of the code. Six months later, a teammate opens the repo and messages you: "How do I even run this thing?" You've already forgotten. Documentation isn't bureaucracy — it's a time-travel letter to the next person who needs your code to work. That person is often you.
⚠️ The vibe trap
You built something real. It runs. You know how it works. So you skip the README — or you left the auto-generated one that says "This project was bootstrapped with Create React App." The trap is thinking the code explains itself. It explains what it does. It doesn't explain why you made the choices you made, what the .env should contain, which npm script actually deploys, or why you swapped out the auth library in week three. When you leave those blanks, your future self or your teammate pays the price in hours of archaeology.
📋 The README That Actually Helps
A README isn't a formality — it's an onboarding document. A good one answers five questions before the reader even has to ask.
# Project Name
One sentence: what this thing is and who it's for.
## What it does
Brief description. Link to a live demo if you have one.
## Prerequisites
- Node 20+
- A Supabase project (free tier is fine)
- A Stripe test account
## Run it locally
```bash
git clone https://github.com/you/project
cd project
cp .env.example .env # then fill in your values
npm install
npm run dev
Environment variables
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | yes | From Supabase dashboard → Settings → API |
SUPABASE_SERVICE_ROLE_KEY | yes | Keep secret — never expose in client code |
STRIPE_SECRET_KEY | yes | Use sk_test_... for local dev |
Run the tests
npm test # unit tests
npm run test:e2e # Playwright (needs the dev server running)
Deploy
npx vercel deploy --prod --yes
**Mental model:** Think of the README as the answer to "what do I need to know in the first 15 minutes?" Nothing more, nothing less.
**Why this matters:** Onboarding friction compounds. A README that takes someone from zero to running in under ten minutes is a gift that pays out every time someone new joins, or every time you return after a month away.
**Common mistake:** Documenting the happy path only. Someone will hit an error. Add a "Troubleshooting" section with the two or three errors you actually saw during setup.
---
## 📜 ADRs — Capture the Why, Not Just the What
An Architecture Decision Record (ADR) is a short document that answers: *What decision did we make, and why did we make it instead of the alternatives?*
Without ADRs, you get questions like "Why are we using Zustand instead of Redux?" answered with "I think the person who wrote it just liked it better." That's not good enough when you're deciding whether to change it.
```markdown
# ADR-004: Use Supabase Auth instead of rolling our own
**Date:** 2026-03-12
**Status:** Accepted
**Deciders:** @yourusername
## Context
We need user authentication. Options considered:
1. Supabase Auth (built into our existing Supabase project)
2. NextAuth.js (popular, flexible, more config)
3. Roll our own JWT system
## Decision
Use Supabase Auth.
## Reasons
- We're already using Supabase for the database; zero extra infrastructure
- Supports magic links, OAuth, and email/password out of the box
- Row Level Security integrates directly with `auth.uid()`
- NextAuth would require a separate session store; not worth the overhead at this stage
## Consequences
- We are coupled to Supabase for auth. If we migrate the DB away from Supabase later, auth migration will be non-trivial.
- Supabase Auth's rate limits are generous on free tier; revisit at 10k+ users.
## Rejected alternatives
- Rolling our own: too much attack surface for no benefit
- NextAuth: more flexible but more config; the flexibility isn't needed yet
Mental model: ADRs are like commit messages for decisions that span multiple files. Store them in docs/decisions/ and number them sequentially.
Why this matters: The cost of a bad architectural decision multiplies over time. When you document why you chose something, the next person can evaluate whether those reasons still hold — and change course confidently instead of being afraid to touch it.
Common mistake: Only writing ADRs for big decisions. Write them for medium ones too — "why did we switch from date-fns to dayjs?" is exactly the kind of thing that gets forgotten and re-debated.
💬 Code Comments — Why, Not What
A comment that restates the code is noise. A comment that explains the reasoning is gold.
// BAD: restates the obvious
// multiply price by quantity
const total = price * quantity;
// BAD: lies (the code was changed, the comment wasn't)
// returns null if not found
function getUser(id) {
return users.find(u => u.id === id) ?? { id: null, name: 'Guest' };
}
// GOOD: explains a non-obvious constraint
// Stripe requires amounts in the smallest currency unit (cents for USD).
// Multiply by 100 here; never store dollars as floats in the DB.
const amountInCents = Math.round(price * 100);
// GOOD: explains why we do something surprising
// We check `req.headers['x-forwarded-for']` before `req.socket.remoteAddress`
// because this app sits behind a Vercel reverse proxy. Without this, every
// request appears to come from 127.0.0.1 and our rate limiter breaks.
const clientIp = req.headers['x-forwarded-for']?.split(',')[0] ?? req.socket.remoteAddress;
Mental model: Before writing a comment, ask: "Would a competent developer reading this line already know this?" If yes, skip it. If the answer is "they'd know what but not why," write it.
Why this matters: Vibe coding produces a lot of code you didn't fully reason through step by step — the AI helped you arrive at it. That's completely fine. But it means the why lives in the chat you'll never find again. Capturing it in a comment takes 30 seconds and saves hours.
Common mistake: Updating the code but not the comment. A lying comment is worse than no comment. If you change the logic, change the comment first — that forces you to think about whether the original reasoning still applies.
🗺️ The Docs Hierarchy
Not all documentation lives in the same place. Knowing where to put what makes it easy to find.
README.md ← Start here. Run it, understand it, contribute to it.
CONTRIBUTING.md ← How to open PRs, branch naming, code style, how to run tests.
docs/
decisions/
ADR-001-*.md ← Why we chose X over Y. One file per decision.
ADR-002-*.md
api/
openapi.yaml ← Machine-readable API contract (generate with Swagger/Zod-to-OpenAPI).
src/
(inline comments) ← WHY something works the way it does, not what it does.
Mental model: Imagine a new team member on day one. README gets them running. CONTRIBUTING tells them how to ship. ADRs explain the history. Inline comments handle the surprises inside the code itself. Each layer answers a different question.
Why this matters: When docs are scattered or there's only one big wiki page, nobody knows where to look. A predictable hierarchy means docs actually get found — and maintained.
Common mistake: Putting everything in the README until it's 800 lines long. When a README becomes a novel, nobody reads it. Split it out.
📄 Docs That Rot vs. Docs That Live
Docs that rot: A separate wiki page that explains how to set up the project. A Google Doc describing the API. A Notion page that was accurate in January. These live far from the code, so when the code changes, nobody remembers to update them.
Docs that live: The README.md in the repo. An openapi.yaml file generated from your route types. ADRs that describe decisions that are still in effect. Inline comments in the files they describe. These move with the code, live in version control, and show up in pull request diffs so reviewers can catch staleness.
The rule of thumb: If a doc can be generated automatically from the code, automate it. If it can't be automated, keep it as close to the code as possible — ideally in the same directory, ideally in the same pull request as the change it describes.
Docs that live (close to code, versioned):
✅ README.md in the repo root
✅ CONTRIBUTING.md in the repo root
✅ ADRs in docs/decisions/
✅ OpenAPI spec generated from route definitions
✅ JSDoc/TSDoc on public functions
Docs that rot (far from code, unversioned):
❌ "Architecture overview" Notion page last updated 8 months ago
❌ Slack message explaining a deploy gotcha
❌ A comment in a PR that explains why a weird line exists (move it to inline)
❌ A README that says "TODO: add setup instructions"
Common mistake: Treating documentation as a post-ship task. The best time to write the README section for a new environment variable is when you add it to the code — the context is fresh, it takes two minutes, and it will take twenty minutes to reconstruct later.
🛠️ Your Mission
Take the app you've been building through this track and upgrade its documentation in two steps.
Step 1 — Upgrade the README. Use the skeleton above as your template. Add the prerequisites, the local setup steps with real commands, the env vars table with descriptions, the test command, and the deploy command. If any section doesn't apply yet, write a one-line placeholder so the next person knows it's intentional.
Step 2 — Write one ADR. Think back to a real decision you made while building this app: a library you chose, an architectural pattern you adopted, a tool you switched away from. Write ADR-001 using the template above. Be honest about the trade-offs and what you'd revisit later.
✅ You're done when…
- Your README passes the Pre-Ship Checklist (covers what it is, local setup, env vars, tests, deploy — all accurate and runnable by someone who has never seen the repo)
- Every env var the app uses appears in the README env vars table with a description
- At least one ADR exists in
docs/decisions/describing a real architectural choice with rejected alternatives documented - You have at least two inline comments in your codebase that explain why, not what — and zero comments that restate what the code already says plainly
- Your README is under 150 lines (if it's longer, something belongs in CONTRIBUTING or an ADR)
➡️ Next: Git, Deeply.
Build It Right, Or Don't Build It At All. 🏛️