AGENTS.md, the conventions file
Write AGENTS.md, the single machine-and-human-read file that gives coding agents and new contributors the commands, conventions, and watch-outs they need to work in a repo.
The thin README you built in the last lesson ended with a “Where the docs live” section, and its very first link pointed somewhere: conventions for working in this codebase → AGENTS.md. You wrote that line as a promise. This lesson is the file that pays it off.
Step back and look at the gap the README left open. You made the README thin on purpose, and you sent every reference doc to the source file that owns it: the schema is the data-model doc, env.ts is the env-var doc, the Server Action’s signature is the API doc. That’s tidy, but it leaves a real question unanswered. Where do the conventions live now? The build and test commands, the “we use Server Components by default” rules, the traps a newcomer would otherwise fall into on day one: none of that belongs in the first-contact README, and none of it lives in any single source file. It needs a home.
In 2026 that home has a name: AGENTS.md. It is one plain-markdown file at the root of the repo, written for two readers at once: the coding agents that work in the codebase every day, and the humans who skim it to learn how the team does things. By the end of this lesson you’ll be able to write a high-signal AGENTS.md for a SaaS repo, and you’ll know exactly which facts belong in it rather than in the README, a source file, or an ADR. There’s also a worked example sitting right under your nose: the repo this course ships from has an AGENTS.md at its root. Open it in another tab and read along, and you’ll recognize most of it before this lesson is over.
What AGENTS.md actually is
Section titled “What AGENTS.md actually is”AGENTS.md is a markdown file named exactly that: uppercase, at the repository root. There is no schema, no required headings, no JSON frontmatter, no special syntax. It’s prose, and whatever you write, an agent reads.
That last point is the whole reason it exists. A coding agent dropped into an unfamiliar repo has the same problem a new hire does: it doesn’t know your build command, your conventions, or the rules you’ve all silently agreed to follow. Before it touches a line of code, it reads AGENTS.md to load that context. The standard is real and consolidated: it’s stewarded by the Agentic AI Foundation under the Linux Foundation, and it’s already adopted across tens of thousands of repositories and the major coding tools. This isn’t one vendor’s gimmick you can ignore; it’s the convention.
This leads to the consequence that shapes everything else in the lesson. Because there’s no schema to satisfy and no validator to pass, the file’s entire value is decided by you: by what you choose to put in and, more importantly, what you leave out. The quality metric isn’t completeness or section count. It’s signal per line. A short file where every line earns its place beats a thorough one where half the lines are noise, because the noise doesn’t sit there harmlessly. It buries the lines that matter. That’s why this lesson teaches you a filter rather than a template to fill in.
Two readers, one file
Section titled “Two readers, one file”The mental model worth holding onto is this: there are two readers, and they want the same thing.
Think about what a coding agent needs before it can do useful work. It needs the exact commands to build, test, and lint. It needs to know where things live: which folder holds routes, which holds the data layer. It needs the conventions, so the code it writes matches the code that’s already there. And it needs the watch-outs: the rules that aren’t visible from reading the code, the mistakes that look reasonable but break something three files away. Now think about a human contributor in their first week, asking “how do we actually work in this repo?” The answer is the same list: same commands, same layout, same conventions, same traps.
So you write one file for both, in plain English, with no agent-specific syntax. No “when you are an AI, respond in JSON,” no instruction that only makes sense to a machine. That kind of line is a warning sign: if a sentence wouldn’t help a human contributor, it doesn’t belong in a file a human contributor is supposed to read. Agents parse the same prose a person does, so writing two dialects into one file only makes it worse for everyone.
One framing keeps you honest: write AGENTS.md the way you’d brief a sharp new hire over coffee on their first morning. Be direct, skip the filler, give the non-obvious rules and the exact commands, and add an example only where it actually helps. That voice serves both audiences at once, because both audiences want the same brief.
The diagram below is the picture to hold in your head for the rest of the lesson: the two-readers-one-file idea on top, and underneath it the boundary between AGENTS.md and the two artifacts it sits between.
Two readers, one file
markdown
markdown
Who owns what
What earns a place: the inclusion test
Section titled “What earns a place: the inclusion test”Since there’s no schema telling you what goes in, you need a filter. Here it is, and it’s the most useful sentence in the lesson:
Would a competent developer or agent joining next Monday need this to be productive in week one?
Run every candidate line through that question. If the answer is yes, it goes in. If the answer is no, the line isn’t neutral: it’s noise that pads the file and dilutes the lines that do matter, because the reader now has to wade past it to find them.
A few concrete pairs make the filter concrete. The pnpm test command? Yes, they’ll run it on day one. The team’s Slack channel? No, that’s onboarding trivia, not week-one productivity. “We use Drizzle for all database access”? Yes, it changes how they write code immediately. The history of why you used Prisma before switching? No, that’s an architectural decision, and it belongs in an ADR. The full list of environment variables with their types? No, that already lives in env.ts, so point at it rather than copy it.
That last one should feel familiar. The “could this be a link?” reflex you learned two lessons ago applies inside AGENTS.md, not just to the README. The file is a curated brief, not a vault.
Every section below opens by applying this test. Watch how each one earns its place before you see its content, because the goal is to learn the filter, not to memorize a list of headings to copy.
The sections a SaaS AGENTS.md usually has
Section titled “The sections a SaaS AGENTS.md usually has”There’s no required structure, but a handful of sections show up in most SaaS AGENTS.md files because most SaaS repos have the same week-one needs. We’ll build the file one section at a time, each motivated by the inclusion test and each shown as the small markdown snippet it actually is, and then assemble the whole thing at the end.
Project overview
Section titled “Project overview”Does a newcomer need this in week one? Yes: one paragraph of orientation so everything after it has context.
This is the same content as the README’s opening, just terser. One or two sentences cover what the product does, who uses it, and the stack core. No more.
Multi-tenant invoice management SaaS. Organizations sign up, inviteteammates, and issue invoices to their customers. Stack: Next.js 16(App Router), Postgres via Drizzle, Better Auth, Stripe billing.This is one of exactly two things AGENTS.md deliberately shares with the README. We’ll come back to why that duplication is acceptable when we draw the boundaries.
Repo layout
Section titled “Repo layout”Does a newcomer need this in week one? Yes: they can’t navigate the codebase without it, and neither can an agent.
This section is a directory listing, so render it as one. The folders below are the course’s standard layout, and the dimmed comments are the entire explanation a reader needs.
Directorysrc/
Directoryapp/ Next.js routes (Server Components by default)
- …
Directorycomponents/ shared React components
- …
Directorydb/ Drizzle schema, relations, and the client
- schema.ts source of truth for all tables
Directorylib/ pure helpers and side-effect adapters
- …
Directoryserver/ Server Actions (mutations)
- …
- env.ts validated environment variables
Directorytests/ integration tests against real Postgres
- …
Notice there are no per-file novels here, just enough to point someone at the right folder. The detail lives in the folders; this is a map, not a tour.
Build, test, and lint commands
Section titled “Build, test, and lint commands”Does a newcomer need this in week one? Yes: this is the most-used section in the file. It’s the first thing an agent looks for and the first thing a new hire copies.
List the exact commands plainly, with no narration between them. The commands are the signal, and sentences would only dilute it.
## Commands
pnpm install # install dependenciespnpm dev # start the dev serverpnpm test # run the test suitepnpm db:push # push schema changes to the local databasepnpm db:seed # seed local datapnpm db:generate # generate a migration from schema changespnpm db:migrate # apply pending migrationspnpm db:reset # drop and recreate the local databaseThese are the same script names your README’s “Common tasks” section already used, and that’s the point. Both files reflect a single source of truth: the scripts block in package.json. The README isn’t copying AGENTS.md, and AGENTS.md isn’t copying the README; both are reading package.json. If a script name changes there, both files update in the same breath. Two docs, one truth: the discipline from the last lesson, applied again.
Conventions
Section titled “Conventions”Does a newcomer need this in week one? Yes: this is what stops their first pull request from getting bounced for breaking rules they couldn’t have known.
These are the non-obvious decisions, one line each. The density is the whole value: a reader scans the list, absorbs the house style in fifteen seconds, and writes code that fits.
## Conventions
- Server Components by default; Client Components only at the leaves.- Server Actions for mutations; route handlers only for third-party webhooks.- Zod schemas live next to the action that uses them.- Never `any` — use `unknown` and narrow at the boundary.- Import via `@/`; no deep relative paths.- A file that exports one thing is named after it, kebab-cased.One note in case you second-guess this section: a list of one-line conventions looks like over-commenting if you judge it by normal code-review standards, where comments are rare and earn their keep. But this file’s job is to state conventions. Restating them in one-liners here isn’t redundant; it’s the deliverable. The rule “code should explain itself, comments are for the non-obvious why” governs your .ts files. AGENTS.md is the one place the conventions get written down on purpose.
What NOT to do
Section titled “What NOT to do”Does a newcomer need this in week one? Yes, and this is the section people underrate.
A competent newcomer’s mistakes are predictable. There’s a short list of things that look perfectly reasonable but break something in your specific codebase, and naming them up front is pure signal: you spend three lines to save a failed run, a confusing error, or a burned review cycle. This is negative space, and it earns its place precisely because the failure modes are foreseeable.
## Don't
- Don't import server modules into Client Components — they leak secrets. Next.js catches it, but naming it saves a wasted run.- Don't write raw SQL outside Drizzle.- Don't reach for `useEffect` to fetch data on the server — fetch in a Server Component.- Don't ship `console.log` — the `no-console` lint rule fails CI.Keep this section to rules, not reasons. Notice the third line just says not to do it, rather than the full argument for why Server Components are the better data-fetching model. That argument was a whole chapter. When a “don’t” exists because of a real architectural decision (“don’t write raw SQL, we chose Drizzle”), state the rule here and link the why to the ADR. If you find yourself writing a paragraph of justification, you’ve drifted out of AGENTS.md and into ADR territory.
PR and commit conventions
Section titled “PR and commit conventions”Does a newcomer need this in week one? Yes, lightly: they’ll open a pull request that week, so they need the house rules, but only the rules, not a process essay.
## Pull requests
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`.- CI must be green before merge; no force-pushing to `main`.That’s the whole section. The depth of how to review a pull request is a topic in its own right and lives elsewhere; here you just need the contract a contributor follows to get one merged.
Pointers
Section titled “Pointers”Does a newcomer need this in week one? Yes, but as links, not copies. This section is the “could this be a link?” reflex made into a heading.
## Pointers
- Architectural decisions: /docs/adr/- Data model: src/db/schema.ts- Environment variables: src/env.tsThis is the seam where AGENTS.md hands off to the docs that live next to the truth. The env-var list isn’t here; it’s a link to env.ts, because copying it in would create a second copy that drifts the moment a variable changes. The schema isn’t here; it’s a link to schema.ts. The reasoning behind your architectural choices isn’t here; it’s a link to the ADR log. AGENTS.md states the conventions and points at the canonical source for everything else.
The whole thing, assembled
Section titled “The whole thing, assembled”Now read the file as one piece. This is the assembled AGENTS.md for the invoice SaaS: the sections you just built, in order, as a single coffee-brief. Step through it and notice how each section does exactly one job and stops.
Multi-tenant invoice management SaaS. Organizations sign up, inviteteammates, and issue invoices to their customers. Stack: Next.js 16(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)- src/components — shared React components- src/db — Drizzle schema, relations, and the client- src/server — Server Actions (mutations)- src/lib — pure helpers and side-effect adapters- tests — integration tests against real Postgres
## Commands
pnpm install # install dependenciespnpm dev # start the dev serverpnpm test # run the test suitepnpm db:push # push schema changes to the local databasepnpm db:seed # seed local datapnpm db:migrate # apply pending migrationspnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.- Server Actions for mutations; route handlers only for webhooks.- Zod schemas live next to the action that uses them.- Never `any` — use `unknown` and narrow at the boundary.- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.- Don't write raw SQL outside Drizzle.- Don't reach for `useEffect` to fetch server data.- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/- Data model: src/db/schema.ts- Environment variables: src/env.tsOne paragraph of orientation: what, who, the stack core. Same content as the README’s opening, but terser. Everything after it now has context.
Multi-tenant invoice management SaaS. Organizations sign up, inviteteammates, and issue invoices to their customers. Stack: Next.js 16(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)- src/components — shared React components- src/db — Drizzle schema, relations, and the client- src/server — Server Actions (mutations)- src/lib — pure helpers and side-effect adapters- tests — integration tests against real Postgres
## Commands
pnpm install # install dependenciespnpm dev # start the dev serverpnpm test # run the test suitepnpm db:push # push schema changes to the local databasepnpm db:seed # seed local datapnpm db:migrate # apply pending migrationspnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.- Server Actions for mutations; route handlers only for webhooks.- Zod schemas live next to the action that uses them.- Never `any` — use `unknown` and narrow at the boundary.- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.- Don't write raw SQL outside Drizzle.- Don't reach for `useEffect` to fetch server data.- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/- Data model: src/db/schema.ts- Environment variables: src/env.tsA map, not a tour. Just enough to point a person or an agent at the right folder. The detail lives inside the folders.
Multi-tenant invoice management SaaS. Organizations sign up, inviteteammates, and issue invoices to their customers. Stack: Next.js 16(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)- src/components — shared React components- src/db — Drizzle schema, relations, and the client- src/server — Server Actions (mutations)- src/lib — pure helpers and side-effect adapters- tests — integration tests against real Postgres
## Commands
pnpm install # install dependenciespnpm dev # start the dev serverpnpm test # run the test suitepnpm db:push # push schema changes to the local databasepnpm db:seed # seed local datapnpm db:migrate # apply pending migrationspnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.- Server Actions for mutations; route handlers only for webhooks.- Zod schemas live next to the action that uses them.- Never `any` — use `unknown` and narrow at the boundary.- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.- Don't write raw SQL outside Drizzle.- Don't reach for `useEffect` to fetch server data.- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/- Data model: src/db/schema.ts- Environment variables: src/env.tsThe most-used section. Exact commands, no narration. These mirror package.json, the same source the README’s tasks read from.
Multi-tenant invoice management SaaS. Organizations sign up, inviteteammates, and issue invoices to their customers. Stack: Next.js 16(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)- src/components — shared React components- src/db — Drizzle schema, relations, and the client- src/server — Server Actions (mutations)- src/lib — pure helpers and side-effect adapters- tests — integration tests against real Postgres
## Commands
pnpm install # install dependenciespnpm dev # start the dev serverpnpm test # run the test suitepnpm db:push # push schema changes to the local databasepnpm db:seed # seed local datapnpm db:migrate # apply pending migrationspnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.- Server Actions for mutations; route handlers only for webhooks.- Zod schemas live next to the action that uses them.- Never `any` — use `unknown` and narrow at the boundary.- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.- Don't write raw SQL outside Drizzle.- Don't reach for `useEffect` to fetch server data.- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/- Data model: src/db/schema.ts- Environment variables: src/env.tsThe house style in five lines. This is the high-signal core: the list that stops a newcomer’s first PR from breaking rules they couldn’t have known.
Multi-tenant invoice management SaaS. Organizations sign up, inviteteammates, and issue invoices to their customers. Stack: Next.js 16(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)- src/components — shared React components- src/db — Drizzle schema, relations, and the client- src/server — Server Actions (mutations)- src/lib — pure helpers and side-effect adapters- tests — integration tests against real Postgres
## Commands
pnpm install # install dependenciespnpm dev # start the dev serverpnpm test # run the test suitepnpm db:push # push schema changes to the local databasepnpm db:seed # seed local datapnpm db:migrate # apply pending migrationspnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.- Server Actions for mutations; route handlers only for webhooks.- Zod schemas live next to the action that uses them.- Never `any` — use `unknown` and narrow at the boundary.- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.- Don't write raw SQL outside Drizzle.- Don't reach for `useEffect` to fetch server data.- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/- Data model: src/db/schema.ts- Environment variables: src/env.tsPredictable mistakes, pre-empted. Three lines to save a failed run. Rules only; the why links out to an ADR.
Multi-tenant invoice management SaaS. Organizations sign up, inviteteammates, and issue invoices to their customers. Stack: Next.js 16(App Router), Postgres via Drizzle, Better Auth, Stripe billing.
## Layout
- src/app — Next.js routes (Server Components by default)- src/components — shared React components- src/db — Drizzle schema, relations, and the client- src/server — Server Actions (mutations)- src/lib — pure helpers and side-effect adapters- tests — integration tests against real Postgres
## Commands
pnpm install # install dependenciespnpm dev # start the dev serverpnpm test # run the test suitepnpm db:push # push schema changes to the local databasepnpm db:seed # seed local datapnpm db:migrate # apply pending migrationspnpm db:reset # drop and recreate the local database
## Conventions
- Server Components by default; Client Components only at the leaves.- Server Actions for mutations; route handlers only for webhooks.- Zod schemas live next to the action that uses them.- Never `any` — use `unknown` and narrow at the boundary.- Import via `@/`; no deep relative paths.
## Don't
- Don't import server modules into Client Components.- Don't write raw SQL outside Drizzle.- Don't reach for `useEffect` to fetch server data.- Don't ship `console.log` — the `no-console` lint rule fails CI.
## Pointers
- Architectural decisions: /docs/adr/- Data model: src/db/schema.ts- Environment variables: src/env.tsThe handoff. Links to the schema, env, and ADRs, never copies. The “could this be a link?” reflex, made into a section.
That’s a complete AGENTS.md, and it fits on one screen. Compare it to the course’s own root AGENTS.md. It’s shaped a little differently, because there’s no single template, only the filter. Both are short, both are scannable, and neither has a section that’s there “for completeness.”
Nested files: the nearest one wins
Section titled “Nested files: the nearest one wins”One rule about where AGENTS.md lives is worth knowing before you hit the case that needs it.
When an agent edits a file, it doesn’t read every AGENTS.md in the repo. It walks up the directory tree from the file it’s working on and uses the nearest one. Here’s the part people get wrong: the nearest file wins outright, because the files are not merged. A closer AGENTS.md doesn’t layer on top of the root one; it replaces it for everything under its folder.
That’s what makes overrides possible. A monorepo, or one idiosyncratic corner of a codebase, can ship its own AGENTS.md with conventions that differ from the root. The root sets the sensible defaults, and a subfolder overrides them where it genuinely needs to.
- AGENTS.md repo-wide defaults
Directorypackages/
Directoryemail-templates/
- AGENTS.md overrides for working on email templates
The course’s stack is a single app, so the root AGENTS.md is the only one you need today. But the day you split out a packages/ directory, this is the rule that lets each package carry its own brief without contradicting the root, and it’s why some very large repositories run dozens of AGENTS.md files, one per area, each winning within its own folder.
Where AGENTS.md ends and the other docs begin
Section titled “Where AGENTS.md ends and the other docs begin”The diagram earlier previewed three boundaries. Now make them sharp, because keeping them straight is what stops your four documentation artifacts from sprawling into each other.
Versus the README. The README is for first contact: the recruiter and the new contributor in their first hour. AGENTS.md is for the agent and the contributor doing real work in their first week. They intentionally share exactly two things: the one-paragraph project overview and the local-setup commands. That overlap is deliberate. Both pieces are first-hour and first-week needs, both reflect the same source (the product description and package.json), and a reader of either file shouldn’t have to jump to the other just to get a dev server running. After those two shared pieces they diverge sharply: the README links out, while AGENTS.md goes deep on conventions.
Versus ADRs. This is the boundary people blur most. AGENTS.md states what the convention is: “Drizzle for all database access.” An ADR records why that decision was made and what it cost: “Drizzle over Prisma, because we wanted control over the generated SQL and a smaller client, at the price of a less mature plugin ecosystem.” AGENTS.md links to the ADR for the reader who wants the reasoning. What you must not do is put the reasoning in AGENTS.md. The moment you start writing “we chose Drizzle because of these trade-offs,” you’ve bloated the conventions file and duplicated content that has a proper home. State the convention, and link the why.
Versus source-as-doc. The reference material lives in the source file that owns it: the env-var list in env.ts, the columns and constraints in schema.ts, the API contract in the Server Action’s TSDoc. AGENTS.md points at all of them. This is the same rule that kept the README thin, applied one layer in: the conventions file governs itself with “could this be a link?” just like the README does.
There are two failure modes to name, and they’re the predictable ones. The first is treating AGENTS.md as the junk drawer, the home for “everything that doesn’t fit anywhere else.” Do that and the file balloons, the signal drops, and you’re back to a document nobody reads past line 50. The second is duplicating the env-var list (or the schema, or the ADR rationale) into AGENTS.md instead of linking it. Two copies of the same truth means one of them is wrong within a month, and you won’t know which.
The exercise below is the skill itself: routing a fact to the document that owns it.
Each fact below belongs in exactly one document. Sort it into the doc that owns it — the canonical home, not just a place it could appear. Drag each item into the bucket it belongs to, then press Check.
pnpm test commandinvoices table’s columns and constraintsSignal density: writing the file a newcomer actually reads
Section titled “Signal density: writing the file a newcomer actually reads”You now know what goes where. The last piece is the writing posture, and it’s the same one the whole lesson has been circling.
The spec mandates no sections precisely because the value isn’t structure, it’s signal per line. So write AGENTS.md the way you’d brief that new hire over coffee: direct, no filler, an example only where it earns its place. The dominant failure mode is the file that’s been “expanded for completeness,” with every conventional section present and full and not one of them high-signal. The fix is subtractive. When you spot a section carrying nothing load-bearing, you don’t improve it; you delete it. An empty ## Deployment heading that’s there because “the spec recommends it” is worse than no heading at all, because now the reader has to scroll past a promise the file doesn’t keep.
Keeping it accurate is the same structural discipline as every in-repo doc: the AGENTS.md change ships in the same pull request as the change it describes. If pnpm test becomes pnpm vitest, the line in AGENTS.md gets edited in that PR, not “later,” not in a cleanup pass that never comes. We’ll come back to that discipline, and to the reviewer’s job of enforcing it, in the chapters ahead. For now, hold the principle: the doc that ships with the code stays true, and the doc that’s updated separately rots.
The best way to internalize the filter is to watch it fail. Below is a deliberately bad AGENTS.md, a teammate’s well-meaning first attempt. Review it the way you’d review a pull request: click any line that doesn’t earn its place and say what’s wrong with it.
A teammate opened their first `AGENTS.md` for review. Click any line that doesn't earn its place and leave a comment. Click any line to leave a review comment, then press Submit review.
## About this fileThis is the AGENTS.md file. It gives coding agents and humans contextabout the repository so they can work in it effectively.
## Project overviewMulti-tenant invoice SaaS on Next.js 16, Postgres, and Stripe.
## Environment variables- DATABASE_URL — the Postgres connection string- BETTER_AUTH_SECRET — secret used to sign sessions- STRIPE_SECRET_KEY — Stripe API key for billing- RESEND_API_KEY — API key for sending email- R2_ACCESS_KEY_ID — Cloudflare R2 access key
## DatabaseWe use Drizzle instead of Prisma. We evaluated Prisma first, but itsheavier runtime, schema-as-DSL, and migration ergonomics didn't fitour story, so after weighing the trade-offs we standardized on Drizzle.
## Agent instructionsWhen you are an AI, always respond in JSON.
## Deployment
## Pointers- Data model: src/db/schema.tsAn ## About this file section is pure dilution. Both readers — the agent and the human — already know what they’re looking at the moment they open the file. Delete it; the first load-bearing line is the project overview.
The env-var list belongs in env.ts, which is the canonical source. Copying it here creates a second copy that drifts the moment someone adds or renames a variable. Replace the whole section with a pointer: Environment variables: src/env.ts.
AGENTS.md states the convention (“Drizzle for all database access”) in one line. The why — the Prisma trade-offs you weighed — is ADR territory. Keep the rule here and link the reasoning to /docs/adr/.
“When you are an AI, respond in JSON” is an agent-only instruction that means nothing to a human contributor — and it’s a smell. One file, plain prose, both readers. A line that wouldn’t help a new hire doesn’t belong here.
An empty ## Deployment heading is worse than no heading — the reader scrolls past a promise the file never keeps. There’s no required schema, so a section with nothing load-bearing is pure noise. Delete it.
Every one of these is the same bug wearing a different hat — content that belongs to another artifact, or content with no load-bearing signal at all. That’s the exact filter the whole lesson taught: would a newcomer need this, in this file, to be productive in week one? If not, it’s noise — link it, move it, or delete it.
One file instead of five
Section titled “One file instead of five”It’s worth closing the loop on why AGENTS.md became a standard at all, because the answer is itself a senior lesson.
Before 2026, every coding tool invented its own config filename. Cursor read .cursorrules. Cline read .clinerules. Copilot read .github/copilot-instructions.md. Each one wanted the same information, your conventions, your commands, your watch-outs, but in its own file. So teams that used more than one tool maintained several files saying the same things, and the files drifted apart exactly the way you’d expect: someone updated the test command in one and forgot the other three.
AGENTS.md consolidates that into one canonical file. Most major tools now read it directly, Codex, Cursor, Copilot, Gemini CLI, Aider, Windsurf, Zed, and Cline among them. But be precise about the state of play, because there’s one important wrinkle: not every tool reads it natively yet. CLAUDE.md is the notable holdout, since Claude Code reads CLAUDE.md, not AGENTS.md. The honest 2026 story isn’t “every tool reads AGENTS.md natively”; it’s “one canonical file, with a one-line bridge for the tools that haven’t caught up.”
And that bridge is the senior move. You maintain one AGENTS.md as the single source of truth. If a tool insists on its own filename, you don’t write it a second full instruction file; you make that file a one-line pointer to AGENTS.md, or a symlink. The course’s own repo does exactly this: open its CLAUDE.md and you’ll find it’s essentially a single @AGENTS.md import line. Two full instruction files are two things to keep in sync, which means one of them will be wrong. A bridge has nothing to drift.
That’s the principle the chapter keeps returning to, in its smallest form: one source of truth, and everything else points at it.
Wrapping up
Section titled “Wrapping up”AGENTS.md is the brief you’d give a sharp new hire over coffee: the commands, the layout, the conventions, the traps. The difference is that it’s machine-read too, so one file serves the agent and the human at once. There’s no schema, which means the file’s value is signal per line, set entirely by your judgment. The filter that governs every line is one question: would a competent newcomer need this, in this file, to be productive in week one? Yes goes in; no is noise.
And it stays in its lane. The README owns first contact, AGENTS.md owns conventions and commands, ADRs own the why, and the source files own the reference. AGENTS.md states what the conventions are and links out for everything else, the env list to env.ts and the reasoning to the ADR log, keeping itself thin with the same “could this be a link?” reflex that kept the README thin.
You’ve now built three of the chapter’s four artifacts and drawn the lines between them. The one boundary you kept hitting, “that’s a convention here, but the why lives in an ADR,” is the next lesson. You’ll learn the ADR itself: the short, one-decision-per-file record of why a choice was made, built on the real decisions this course has already shipped.