AGENTS.md, the conventions file
Write AGENTS.md, the single file that briefs coding agents and new contributors on a repo's commands, conventions, and watch-outs.
Last lesson’s thin README routed every reference doc to the source that owns it: the schema is the data-model doc, env.ts documents the env vars, a Server Action’s signature is its own API doc. That leaves the conventions homeless, the build and test commands, the “Server Components by default” rules, the traps a newcomer hits on day one. None of it belongs in a first-contact README, and none of it lives in any single source file.
Its home is AGENTS.md: one plain-markdown file at the repo root, written for two readers at once, the coding agents that work in the codebase daily and the humans who skim it to learn how the team works. By the end of this lesson you’ll write a high-signal AGENTS.md for a SaaS repo and know which facts belong in it rather than the README, a source file, or an ADR. This course ships from a repo that has one at its root, so open it in another tab and read along.
What AGENTS.md is
Section titled “What AGENTS.md is”AGENTS.md is a markdown file, named exactly that, at the repository root. There’s no schema, no required headings, no special syntax: it’s prose, and whatever you write, an agent reads.
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. It reads AGENTS.md to load that context before touching a line of code. This is a real, adopted standard, used across tens of thousands of repos and the major coding tools.
With no schema to satisfy and no validator to pass, the file’s value rests entirely on what you put in and leave out. The 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 noise buries the lines that matter.
Two readers, one file
Section titled “Two readers, one file”Before a coding agent can do useful work it needs the exact commands to build, test, and lint; where things live, like which folder holds routes and which holds the data layer; the conventions, so its code matches what’s already there; and the watch-outs, the rules you can’t see by reading the code. A human contributor in their first week needs that same list.
So you write one file for both, in plain English with no agent-specific syntax. Agents parse the same prose a person does, and a sentence that wouldn’t help a human doesn’t belong in a file a human is meant to read.
Brief AGENTS.md the way you’d brief a sharp new hire over coffee: be direct, skip the filler, give the non-obvious rules and the exact commands, and add an example only where it helps.
Two readers, one file
markdown
markdown
Who owns what
What belongs in the file: the inclusion test
Section titled “What belongs in the file: the inclusion test”With no schema to tell you what goes in, you need a filter:
Would a competent developer or agent joining next Monday need this to be productive in week one?
Run every candidate line through it. If yes, it goes in. If no, the line isn’t neutral: it’s noise that dilutes the lines that matter, because the reader has to wade past it to reach them.
See the filter at work:
- The
pnpm testcommand: yes, they 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.
- Why you used Prisma before switching: no, that’s an architectural decision and 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 is the “could this be a link?” reflex from two lessons ago, applied inside AGENTS.md. The file is a curated brief, not a vault.
The common sections of a SaaS AGENTS.md
Section titled “The common sections of a SaaS AGENTS.md”There’s no required structure, but a few sections show up in most SaaS AGENTS.md files, because most SaaS repos share the same week-one needs. We’ll build the file one section at a time, each justified by the inclusion test, then assemble it.
Project overview
Section titled “Project overview”One paragraph of orientation, so everything after it has context: a sentence or two on what the product does, who uses it, and the stack core. It’s the README’s opening, just terser.
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 return to why that duplication is acceptable when we draw the boundaries.
Repo layout
Section titled “Repo layout”Neither a newcomer nor an agent can navigate the codebase without a map. This section is a directory listing, so render it as one; the dimmed comments are the whole explanation.
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
- …
A map, not a tour: enough to point someone at the right folder, with the detail living in the folders.
Build, test, and lint commands
Section titled “Build, test, and lint commands”The most-used section, the first thing an agent looks for and a new hire copies. List the exact commands plainly; narration between them would only dilute the signal.
## 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 script names from your README’s “Common tasks” section, because both files read from one source of truth: the scripts block in package.json. Rename a script there and both docs update in the same breath.
Conventions
Section titled “Conventions”This is what stops a newcomer’s first pull request from getting bounced for rules they couldn’t have known. State the non-obvious decisions, one line each; the density is the 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.By normal code-review standards this looks like over-commenting. But stating conventions is this file’s job: the one-liners aren’t redundant, they’re the deliverable. “Code explains 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”The section people underrate. A competent newcomer’s mistakes are predictable: things that look reasonable but break something in your specific codebase. Naming them up front is pure signal, three lines that save a failed run, a confusing error, or a burned review cycle.
## 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 to rules, not reasons. When a “don’t” rests on a real architectural decision, state the rule here and link the why to the ADR; a paragraph of justification means you’ve drifted into ADR territory.
PR and commit conventions
Section titled “PR and commit conventions”They’ll open a pull request that week, so they need the house rules, but only the rules.
## Pull requests
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`.- CI must be green before merge; no force-pushing to `main`.How to review a pull request is a topic of its own and lives elsewhere; here you just need the contract for getting one merged.
Pointers
Section titled “Pointers”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, the schema, and your architectural reasoning aren’t copied here; each is a link, because a copy drifts the moment the source changes.
The whole thing, assembled
Section titled “The whole thing, assembled”Now read the file as one piece: the sections you built, in order, as a single coffee-brief for the invoice SaaS. 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. The README’s opening, 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, mirroring 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: the high-signal core 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.
A complete AGENTS.md that fits on one screen. Compare it to the course’s own root AGENTS.md: 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”An agent editing a file doesn’t read every AGENTS.md in the repo. It walks up the directory tree and uses the nearest one. The files are not merged: a closer AGENTS.md replaces the root for everything under its folder rather than layering on top. That is what makes overrides work: the root sets sensible defaults, and one corner of the codebase, say a monorepo package, can ship its own AGENTS.md where it needs different rules.
- 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 all you need today. The day you split out a packages/ directory, each package can carry its own brief without contradicting the root.
Where AGENTS.md ends and the other docs begin
Section titled “Where AGENTS.md ends and the other docs begin”Three sharp boundaries keep your docs from sprawling into each other.
Versus the README. The README serves first contact in the reader’s first hour; AGENTS.md serves real work in their first week. They share exactly two things, the one-paragraph overview and the local-setup commands. After that they diverge: the README links out, 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 and what it cost: “Drizzle over Prisma, for control over the generated SQL and a smaller client, at the price of a less mature plugin ecosystem.” AGENTS.md links to the ADR; it never carries the reasoning. Write “we chose Drizzle because of these trade-offs” and you’ve bloated the conventions file with content that has a proper home.
Versus source-as-doc. 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 links to all of them.
Two failure modes recur. The first treats AGENTS.md as the junk drawer for everything that fits nowhere else; the file balloons and nobody reads past line 50. The second copies a reference list in instead of linking it; two copies of one truth means one is wrong within a month, and you won’t know which.
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: cut sections, don’t pad them
Section titled “Signal density: cut sections, don’t pad them”The remaining skill is the writing posture, and it’s subtractive. The spec mandates no sections because the value isn’t structure, it’s signal per line. The dominant failure mode is the file “expanded for completeness”: every conventional section present, full, and none high-signal. When a section carries nothing load-bearing, delete it. An empty ## Deployment heading left in because “the spec recommends it” is worse than none: the reader scrolls past a promise the file never keeps.
Accuracy follows the same discipline as any in-repo doc: the AGENTS.md edit ships in the same pull request as the change it describes. When pnpm test becomes pnpm vitest, the line gets fixed in that PR, not in a later cleanup that never comes.
To internalize the filter, watch it fail. Below is a deliberately bad AGENTS.md, a teammate’s well-meaning first attempt. Review it like a pull request: click any line that doesn’t earn its place and say what’s wrong.
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”Coding tools used to each invent their own config filename, so a team running more than one maintained several files saying the same thing, and they drifted: someone updated the test command in one and forgot the rest.
AGENTS.md consolidates that into one canonical file, now read directly by most major tools, including Codex, Cursor, Copilot, Gemini CLI, Aider, Windsurf, Zed, and Cline. The notable holdout is CLAUDE.md : Claude Code reads it, not AGENTS.md.
When a tool insists on its own filename, keep AGENTS.md as the single source of truth and make that file a one-line pointer to it, or a symlink . This course’s repo does exactly that: its CLAUDE.md is essentially a single @AGENTS.md import line. Two full instruction files drift; a bridge has nothing to drift.
Wrapping up
Section titled “Wrapping up”With no schema to lean on, every line earns its place by one test: would a competent newcomer need this, here, to be productive in week one? Everything else links out, the reasoning to the ADR log, which is the next lesson: a short record of why a single decision was made.