Boring Docs

Lock Checklist — Criteria for v1.0-locked

Green gates before freezing verbs forever.

Lock Checklist (v1.0-locked)

Do not tag until every box is green. I know the temptation — the demo works, a friend wants the repo, "we'll fix the rest later." Later never comes, and an unlocked v1 means every future feature negotiates with the past. This checklist is the negotiation, done once, in advance. Go box by box. Each one below tells you what to run, what green looks like, and what midnight looks like if you skip it.

Goal: frozen verbs, just-works binary. After the tag, new capability means a new provider file + one registry line + parity test. No new pages, no new top-level folders. The freeze is what lets agents (and future-you) build without fear — the contract can't shift under them.

P0 — money + data (skip these and lose revenue or rows)

  • Single webhook truth per rail via access.js, dedupe on providerPaymentRef (unique everywhere), 4xx on bad signature, 500 on failure so providers retry. Verify: replay the same webhook twice — second returns { duplicate: true }, one order row exists. Send an unsigned payload — 401, nothing written. Skip it and: double-charges during provider outages, or silent misroutes you discover from an angry email, not your logs.
  • Checkout price from product row only; priceId/mode allowlisted; redirect URLs relative same-origin. Verify: edit the checkout HTML in devtools to say $1, complete the flow, confirm the ledger says full price. Skip it and: you will get robbed by a teenager with devtools. This is not hypothetical; it's the oldest trick in the book.
  • Cancel binds subscription → order buyer/customer or admin (403 else). Verify: as user A, call cancel with user B's subscription ID — 403. Skip it and: anyone cancels anyone. Yes, really.
  • Claim-repo session-bound + username regex; githubUsername persists on all DBs. Verify: invalid usernames rejected, invite fires on grant. Skip it and: junk rows and failed invites you'll clean by hand.
  • Order pricingType + unique refs on all three engines; SQLite/Supabase/Mongo smoke-passing. Verify: npm run test:matrix green on all three. Skip it and: the engine you didn't test is the one prod runs. Guess how I know.
  • Clean-build boot; /db volume + SQLITE_PATH verified; backups include analytics.sqlite. Verify: fresh clone → build → boot → smoke test, with zero local state carried over. Confirm the backup set contains both databases. Skip it and: "works on my machine" becomes "down on theirs," and the analytics history you forgot to back up is gone with the disk.

P1 — auth + abuse + rendering (skip these and lose trust)

  • Stale sessions → /auth without loops; admin fail-closed in prod. Verify: delete your user row, refresh with the cookie — login form, not a bounce loop. Hit admin routes as non-admin — 403 with a message. Skip it and: support tickets that say "login is broken" with nothing in your logs.
  • In-app rate limits on auth/lead/vote/track/webhooks (nginx alone isn't enough — in-app limits follow the code when it moves hosts). Verify: hammer /api/lead 30 times in a minute — 429s appear. Skip it and: bot farms harvest your invite/lead endpoints within a week of launch. They scan for exactly this.
  • Markdown HTML sanitized + CSP; no fake SEO ratings/prices. Verify: grep dangerouslySetInnerHTML — every hit commented with why its input is trusted. Ratings and prices in schema match visible content (Google penalizes mismatches). Skip it and: stored XSS the day you open markdown to users, plus rich-result penalties.
  • Lead validation + idempotent 200s; vote race handled gracefully. Verify: double-submit the capture form — one row, success both times. Rapid-fire votes — counts stay exact. Skip it and: duplicate rows and off-by-N counts you'll reconcile by hand at the worst moment.

P2 — hygiene (skip these and lose velocity)

  • images.remotePatterns, deps upgraded (npm audit clean on high/critical). Verify: audit output saved next to the tag. Skip it and: a known CVE in a dependency you forgot you had. Boring means patched.
  • Thin-route (≤40 lines) and table-name docs match code, or docs amended once. Verify: longest route handler under the limit; CONVENTIONS.md table names match the schemas. Skip it and: agents start inventing structure because the map lied. The map is the product — keep it true.
  • scripts/test-matrix.sh: sqlite → supabase → mongo + webhook replay, all green. The full parade, one command. Skip it and: see P0, last box.

Tag (the ceremony)

When every box above is honestly green — not "green except," green:

git add data/docs && git commit -m "docs: boring guides v1" && git tag v1.0-locked

After the tag: verbs frozen. Anyone — you, a contributor, an agent at 2am — can build on this contract knowing it won't shift. That certainty is the entire product. Ceremonies matter: tag it, announce it, then go build the fun stuff on top of the boring foundation.

Re-opening the freeze (because you will need to)

Frozen doesn't mean fossilized. When a real need arrives that the contract can't express — a new capability, not a tweak — follow the thaw ritual so the freeze means something:

  1. Propose in one paragraph: what the new verb is, why existing verbs can't carry it, which providers it touches. If you can't state it in a paragraph, you don't understand it yet.
  2. Additive only: new provider file + one registry line + parity test on all three engines. The thaw never edits existing verbs — it extends beside them. Editing a frozen verb breaks every agent workflow built on it; that's the thing the freeze was preventing.
  3. Re-run the full matrix, update the affected doc sections, and tag v1.1-locked. The version history becomes your changelog: anyone can see exactly when the contract grew and why.

Thaws should be rare (quarterly at most) and celebrated, not snuck in. A contract that changes weekly was never frozen — and then you're back to negotiating with the past on every feature. Protect the freeze; it's the most valuable thing in this repo.

Next: 13-build-features, the manual for everything after the freeze.