TL;DR: The AGENTS.md files from the Document Copilot repo (by Dave Ebbelaar) enforce a strict dependency policy (“write it yourself”), a single-source-of-truth configuration rule, explicit anti-patterns, and stack lock-in — all designed to keep AI coding agents from drifting into mess.
The Three-File Structure
The repo uses a hierarchical AGENTS.md structure — one at the root, one per service:
document-copilot/├── AGENTS.md # Universal rules (stack, deps, config, code style)├── backend/│ └── AGENTS.md # Backend-specific (Python, FastAPI, Alembic, tests)└── frontend/ └── AGENTS.md # Frontend-specific (React, pnpm, Tailwind, shadcn)The root file is the source of truth. The service-level files reference it with “Read ../AGENTS.md first” and add only their own conventions. No duplication.
Nugget 1 — Stack Lock
The root AGENTS.md starts with a locked stack:
Stack is locked unless explicitly changed. Don’t propose alternatives without a stated reason.
This prevents AI agents from suggesting “what about Fastify?” or “have you considered Svelte?” — the most common derailment when working with AI coding tools. The frontend AGENTS.md goes further:
Not Next.js — do not suggest Next, SSR, server components, or file-based routing.
This is critical because AI agents love to “upgrade” your stack to the latest framework. Locking it saves hours of unnecessary refactoring.
Nugget 2 — The Dependency Policy
This is the best dependency policy I’ve seen in an AGENTS.md:
Default: write it yourself. Reach for a library only when the alternative would be non-trivial, error-prone, or reinvention of a standard.
The policy is split into OK to depend on and Not OK:
OK:
- Things genuinely hard to get right (HTTP clients, ASGI servers, SQL drivers, parsers, LLM SDKs, ORM, migrations, auth SDKs)
- The declared stack
Not OK:
- Helper libraries that wrap 5–20 lines of stdlib or platform APIs
- Frameworks where a function would do
- “Nicer API” layers on top of an already-present dependency
Before adding a runtime dependency, the agent must answer in the commit message:
- What exactly does it do that we can’t write in <30 lines of clear code?
- How often does it get used?
- What’s its maintenance / transitive-dep footprint?
The frontend AGENTS.md takes this further with explicit bans:
| Category | Banned | Allowed |
|---|---|---|
| HTTP | axios, ky, got, superagent, redaxios | Native fetch |
| Dates | moment, dayjs, date-fns | Native Date + Intl.DateTimeFormat |
| Utilities | lodash, ramda | Native Array / Object / Map |
| State | External state libraries | useState / useReducer / useContext |
| Forms | External form libraries | Native <form> + FormData |
The frontend also has a pre-install checklist:
- Is there a native browser or TS/JS API that does this?
- Does shadcn/ui already cover it?
- Is it small, well-maintained, and worth the maintenance cost?
Nugget 3 — Supply Chain Defense
The backend uses uv with a 7-day minimum package age:
[tool.uv]add-bounds = "exact"exclude-newer = "7 days"The frontend enforces the same via .npmrc:
minimum-release-age=10080(10,080 minutes = 7 days)
The frontend AGENTS.md explains why:
This defends against typosquat / compromised-release attacks where a malicious version of a popular package goes live and gets pulled within hours.
If a fresh package is genuinely required, it must be overridden per-install and justified in the commit message — never by lowering the global threshold.
Nugget 4 — Configuration: Single Source of Truth
The rule is simple and enforced:
A single settings module is the source of truth for environment per service. Do not call
os.getenv/ readprocess.envdirectly in app code. Do not callload_dotenvanywhere.
The frontend version:
All env reads go through a single
src/lib/env.tsmodule that validates required vars at boot. Never readimport.meta.env.Xdirectly in components.
If a third-party SDK reads os.environ directly, the rule is: mirror them in the settings module — don’t sprinkle setdefault elsewhere.
And: Fail fast on startup if required config is missing. No silent fallbacks that hide real config errors.
Nugget 5 — Code Style: The “Three Lines” Rule
No premature abstraction. Three similar lines is better than a badly-named base class. Extract when there’s a third caller, not a hypothetical one.
This is the anti-XY problem rule for AI agents, who love to create abstractions before there’s a pattern worth abstracting.
Other universal rules:
- Small, obvious functions. A 15-line function with clear names beats a three-class abstraction.
- No error handling for cases that can’t happen. Trust internal callers and framework guarantees.
- Validate only at boundaries: HTTP input, external APIs, DB writes, untrusted parsing.
- No backwards-compat shims unless explicitly asked for.
- No feature flags added speculatively.
- Comments: explain why when non-obvious, never what. Remove stale TODOs.
Nugget 6 — Anti-Patterns (Rejected)
The backend AGENTS.md has an explicit anti-patterns section — things that are actively rejected:
| Anti-Pattern | Why Rejected |
|---|---|
os.getenv / load_dotenv in modules | Centralize in app/config.py |
| Wrapping FastAPI responses in custom envelope classes | Unnecessary indirection |
Over-catching Exception just to log and re-raise | Let it propagate |
| Shared state through globals | Use FastAPI app.state or DI |
| Silent fallbacks that hide real config errors | Fail fast |
| Mocking the LLM in unit tests without testing the grounding contract | The prompt is the product |
The frontend anti-patterns:
| Anti-Pattern | Why Rejected |
|---|---|
Reading import.meta.env.X directly outside lib/env.ts | Single source of truth |
Importing an HTTP library when fetch would do | Dependency policy |
| Mixing client state libraries (Zustand + Jotai + Redux) | One tool, not a zoo |
any annotations to silence the type-checker | TypeScript strict |
| Custom CSS files / styled-components alongside Tailwind | Tailwind only |
| Re-implementing a shadcn primitive by hand | Use what’s shipped |
| Reaching for Next.js, SSR, or any framework requiring a Node server | Plain SPA |
Nugget 7 — Database Migrations: Alembic Rules
The backend AGENTS.md has specific rules for Alembic:
- Alembic is the source of truth. Do not change production tables manually in the Supabase dashboard.
- SQLAlchemy models describe normal tables. Alembic autogenerate creates candidate migrations — but every generated migration must be reviewed before applying.
- Supabase/Postgres-specific features (pgvector extension, HNSW/GIN indexes, RLS policies) belong in explicit migration operations — not in SQLAlchemy models.
- Alembic must use the direct/session database connection, not the Supabase transaction pooler URL.
Nugget 8 — Testing: The Prompt Is The Product
The backend testing rules:
- Prefer unit over integration. Mock at the service boundary.
- Fast suite (
pytest -m "not integration") must stay green and hit no network / no DB. - Integration tests go behind
@pytest.mark.integrationand may require live OpenAI / Supabase credentials. - Tests live next to what they test (
retrieval/retriever.py→tests/retrieval/test_retriever.py). - Required test coverage: ingestion logic, retrieval, citation extraction, grounding enforcement.
The last anti-pattern is the most insightful:
Mocking the LLM in unit tests without also testing the grounding contract — the prompt is the product.
This means: if you mock the LLM and only test that the right tools were called, you’re not testing the actual product. The prompt instructions, the grounding validator, the citation contract — those are the product. They must be tested with real LLM calls.
The frontend has a different philosophy:
No frontend tests. Do not write
*.test.ts/*.test.tsxfiles or introduce a test runner. We verify the frontend manually in the browser pluspnpm tsc --noEmitandpnpm lint.
The reasoning: correctness comes from keeping things simple and well-typed, not from a test suite. If you find yourself reaching for vitest, Playwright, or Cypress — stop. That’s not what this project does.
Nugget 9 — Async by Default
The backend AGENTS.md specifies:
Async by default in request-path code. Don’t run blocking I/O on the event loop. Tempfile + small synchronous file reads are OK (they’re fast); network calls must be async.
This is a practical rule — not dogmatic async everywhere, but async for anything that touches the network.
Nugget 10 — The Backend Module Layout
The backend AGENTS.md prescribes a domain-driven layout rather than a technical one:
app/├── api/ # FastAPI routers├── auth/ # JWT verification + user dependency├── chat/ # Turn orchestration, streaming├── assistant/ # PydanticAI agent, deps, outputs, instructions├── retrieval/ # pgvector/full-text queries, RRF fusion├── grounding/ # Citation validation and grounding checks├── database/ # SQLAlchemy models, Supabase client, typed queries└── prompts/ # Prompt/instruction templatesThe key insight: modules are named after product workflows (chat, assistant, retrieval, grounding), not technical layers (controllers, services, repositories).
Nugget 11 — The Frontend API Client
The frontend AGENTS.md specifies a thin API client in src/lib/api.ts that:
- Handles base URL
- Injects the Supabase bearer token automatically
- Manages timeouts
- Converts failures into typed
ApiErrors (including anisNetworkErrorflag that distinguishes CORS/network from HTTP errors)
The rule: Always use api.get/post/put/patch/delete from @/lib/api — never thread tokens through component props.
Nugget 12 — No Frontend Tests as a Feature
The frontend AGENTS.md explicitly states:
No frontend tests. Do not write
*.test.ts/*.test.tsxfiles or introduce a test runner.
This is a deliberate choice, not an oversight. The reasoning:
- The frontend is a thin UI layer — correctness comes from TypeScript strict mode and Tailwind/shadcn conventions
- Manual browser verification is faster for UI
- Shared logic correctness comes from keeping it simple and well-typed
- If you find yourself reaching for vitest, Playwright, or Cypress — stop
This is controversial but defensible for a small team building an internal tool. The tradeoff is speed vs. safety, and the choice here is speed.
Nugget 13 — Package Manager Enforcement
The frontend AGENTS.md is explicit about pnpm:
pnpmonly. Do not usenpm installoryarn add. The lockfile ispnpm-lock.yaml. If you seepackage-lock.jsonoryarn.lockappear, that’s a bug — delete it.
AI agents commonly use whatever package manager they were trained on most — usually npm. This rule prevents lockfile conflicts.
Summary: The AGENTS.md Pattern
The structure that makes these AGENTS.md files effective:
- Stack declaration — locked, with explicit “don’t suggest alternatives”
- Dependency policy — “write it yourself” default, with explicit OK/Not OK lists
- Configuration — single source of truth, fail fast
- Code style — universal rules + stack-specific rules
- Layout — prescribed directory structure (even if “to be created during build”)
- Anti-patterns — explicit list of rejected patterns
- Testing — clear philosophy (test the product, not the plumbing)
- Supply chain defense — 7-day minimum package age
The most impactful nuggets:
- “The prompt is the product” — test grounding with real LLM calls
- “Three similar lines > a premature generic” — anti-abstraction
- “Write it yourself” default — every dependency is a liability
- Anti-patterns section — AI agents need to know what NOT to do, not just what to do
References
- Build a Full-Stack GenAI Project in 4 Hours (FastAPI, React, Supabase) — Dave Ebbelaar, YouTube (June 6, 2026) — https://www.youtube.com/watch?v=qF5il_9IwME
- Document Copilot Repository — Dave Ebbelaar, GitHub — https://github.com/daveebbelaar/document-copilot
Appendix A — Root AGENTS.md
# Agent Instructions
This file is the source of truth for any coding agent (Claude Code, Cursor, Codex, etc.) working in this repo. Read it before touching code.
## Stack
- **Backend:** Python + FastAPI- **Frontend:** Vite + React SPA + TypeScript- **Database:** Supabase Postgres (users, chats, source documents, chunks)- **Migrations:** SQLAlchemy models + Alembic from the backend- **Retrieval:** Supabase `pgvector` + Postgres full-text search- **Auth:** Supabase Auth- **Hosting:** Railway (backend service + frontend service)- **LLM + embeddings:** OpenAI
Stack is locked unless explicitly changed. Don't propose alternatives without a stated reason.
## Repo layout
```document-copilot/├── AGENTS.md # this file├── README.md├── data/ # local corpus + download script (payloads gitignored)├── docs/ # specs, briefs, design notes├── backend/ # FastAPI service (see backend/AGENTS.md)└── frontend/ # React SPA (see frontend/AGENTS.md)```
## Dependency policy
**Default: write it yourself. Reach for a library only when the alternative would be non-trivial, error-prone, or reinvention of a standard.** Every dependency is a liability — bundle size, supply-chain risk, future upgrade work.
OK to depend on:
- Things that are genuinely hard to get right (HTTP clients, ASGI servers, SQL drivers, parsers, LLM SDKs, ORM, migrations, auth SDKs).- The declared stack (FastAPI, React, Vite, Supabase clients, OpenAI SDK, etc.).
Not OK:
- Helper libraries that wrap 5–20 lines of stdlib or platform APIs.- Frameworks where a function would do.- "Nicer API" layers on top of an already-present dependency.
Before adding a runtime dep, answer in the commit message:
1. What exactly does it do that we can't write in <30 lines of clear code?2. How often does it get used?3. What's its maintenance / transitive-dep footprint?
Per-stack specifics live in `backend/AGENTS.md` and `frontend/AGENTS.md`.
## Configuration
A single settings module is the source of truth for environment per service (`backend/app/config.py`, `frontend/lib/env.ts`). Do not call `os.getenv` / read `process.env` directly in app code. Do not call `load_dotenv` anywhere. If a third-party SDK reads env vars directly, mirror them in the settings module — don't sprinkle `setdefault` elsewhere.
Fail fast on startup if required config is missing. No silent fallbacks that hide real config errors.
## Code style (universal)
- **Small, obvious functions.** A 15-line function with clear names beats a three-class abstraction.- **No premature abstraction.** Three similar lines is better than a badly-named base class. Extract when there's a third caller, not a hypothetical one.- **No error handling for cases that can't happen.** Trust internal callers and framework guarantees. Validate only at boundaries: HTTP input, external APIs, DB writes, untrusted parsing.- **No backwards-compat shims** unless explicitly asked for.- **No feature flags** added speculatively.- **Comments:** explain *why* when non-obvious, never *what*. Remove stale TODOs.- **Keep files focused.** Prefer small modules.Appendix B — Backend AGENTS.md
# Backend — agent notes
This is the FastAPI service for Document Copilot. Read [../AGENTS.md](../AGENTS.md) first — universal building rules live there. This file adds backend-specific conventions.
## Stack
- Python 3.12+- FastAPI + uvicorn- Pydantic v2 + pydantic-settings- `httpx` for outbound HTTP- `pytest` for tests- Supabase Python client (DB + auth)- SQLAlchemy models + Alembic migrations for database schema changes- OpenAI SDK for LLM & embeddings- Supabase `pgvector` for semantic search and Postgres full-text search for keyword retrieval. Hybrid search should run vector and full-text queries separately, then fuse ranked results in Python with Reciprocal Rank Fusion.- `structlog` for logging- `uv` for dependency + project management
## Dependency policy
See universal policy in [../AGENTS.md](../AGENTS.md). Backend-specific:
- **Prefer stdlib:** `pathlib`, `datetime`, `uuid`, `enum`, `dataclasses`, `asyncio`, `collections`, `itertools`, `json`, `urllib`.- **Not OK without justification:** `python-dateutil`, `toolz`, `funcy`, `more-itertools`, small JSON/string micro-libs, "ergonomic" wrappers on top of declared SDKs.- Dev deps (test/lint/build) have a looser bar but still pick widely-used, low-footprint tools (`pytest`, `ruff`, `httpx`).
## Layout (to be created during build)
```backend/├── alembic/│ ├── env.py # Imports app database metadata for autogenerate│ └── versions/ # Reviewed migration files├── alembic.ini├── app/│ ├── main.py # FastAPI entrypoint│ ├── config.py # Pydantic settings — single source of truth for env│ ├── api/ # FastAPI routers (chat, ingest, auth)│ ├── auth/ # Supabase JWT verification + current user dependency│ ├── chat/ # turn orchestration, AI SDK message conversion, streaming│ ├── assistant/ # PydanticAI agent, deps, outputs, instructions│ ├── retrieval/ # pgvector/full-text queries, RRF fusion, source passage lookup│ ├── grounding/ # citation validation and answer grounding checks│ ├── database/ # SQLAlchemy models, Supabase client wrapper, typed query helpers│ └── prompts/ # prompt/instruction templates if not colocated with assistant├── ingest/ # one-off ingestion scripts (Markdown extraction, chunking, embedding, Supabase writes)├── tests/└── pyproject.toml```
## Code style (backend-specific)
- **Type hints on public functions and module-level things.** Don't annotate every local.- **Async by default in request-path code.** Don't run blocking I/O on the event loop. Tempfile + small synchronous file reads are OK (they're fast); network calls must be async.- **Use `async def` for all route handlers** and any I/O service function.- **Validate at boundaries only.** HTTP input is validated by Pydantic models. External API responses are validated when parsed. Internal callers are trusted.
## Configuration
- `app.config.settings` is the single source of truth. Import settings where needed; never call `os.getenv` in app code, never call `load_dotenv`.- If a third-party SDK reads `os.environ` directly, add the mirror in `config.py` — don't sprinkle `setdefault` elsewhere.- Fail fast on startup when required env vars are missing.
## Database migrations
- Alembic is the source of truth for schema changes. Do not change production tables manually in the Supabase dashboard.- SQLAlchemy models describe normal tables and columns. Alembic autogenerate creates candidate migrations, but every generated migration must be reviewed before applying.- Supabase/Postgres-specific features belong in explicit migration operations: `create extension vector`, generated `tsvector` columns, HNSW/GIN indexes, RLS enablement, and RLS policies.- Alembic must use the direct/session database connection, not the Supabase transaction pooler URL.- Run migrations from `backend/` with `uv run alembic upgrade head`.
## Tests
- **Prefer unit over integration.** Mock at the service boundary.- Fast suite (`pytest -m "not integration"`) must stay green and hit no network / no DB.- Integration tests go behind `@pytest.mark.integration` and may require live OpenAI / Supabase credentials.- Tests live next to what they test (`retrieval/retriever.py` → `tests/retrieval/test_retriever.py`).- Required test coverage: ingestion logic, retrieval, citation extraction, grounding enforcement.
## Anti-patterns (rejected)
- `os.getenv` / `load_dotenv` in modules.- Wrapping FastAPI responses in custom envelope classes.- Over-catching `Exception` just to log and re-raise; let it propagate.- Shared state through globals instead of FastAPI `app.state` or DI.- Silent fallbacks that hide real config errors.- Mocking the LLM in unit tests without also testing the grounding contract — the prompt is the product.Appendix C — Frontend AGENTS.md
# Frontend — agent notes
This is the React SPA for Document Copilot. Read [../AGENTS.md](../AGENTS.md) first — universal building rules live there. This file adds frontend-specific conventions.
## Stack
- **Plain React SPA** (Vite + TypeScript, strict). **Not Next.js** — do not suggest Next, SSR, server components, or file-based routing.- **Tailwind CSS** for styling. No CSS modules, styled-components, Emotion, or `.module.css` files for component styles. Global theme tokens live in `src/index.css`.- **shadcn/ui** for UI primitives. Add components with `pnpm dlx shadcn@latest add <name>` — don't hand-roll what shadcn already ships.- **React Router** for routing.- **`@supabase/supabase-js`** for auth (email only — no Google sign-in, no SSO providers).
## Package manager
**`pnpm` only.** Do not use `npm install` or `yarn add`. The lockfile is `pnpm-lock.yaml`. If you see `package-lock.json` or `yarn.lock` appear, that's a bug — delete it.
**Minimum release age: 7 days.** Configured via `.npmrc` (`minimum-release-age=10080` minutes). pnpm will refuse to install any package version published less than 7 days ago. This defends against typosquat / compromised-release attacks where a malicious version of a popular package goes live and gets pulled within hours.
If a fresh package is genuinely required (e.g. urgent security fix in a dep we already use), override per-install and justify in the commit message — don't lower the global threshold.
## Dependency policy
See universal policy in [../AGENTS.md](../AGENTS.md). Frontend-specific:
- **HTTP:** use the native `fetch` API through a thin client in `src/lib/http.ts` and the `api` singleton in `src/lib/api.ts`. **No axios, ky, got, superagent, redaxios.**- **Dates:** use native `Date` and `Intl.DateTimeFormat`. No moment, dayjs, date-fns unless genuinely needed.- **Utilities:** use native `Array` / `Object` / `Map` methods. No lodash, ramda.- **State:** `useState` / `useReducer` / `useContext` first. Only reach for external state libraries when the pain is real.- **Forms:** native `<form>` + `FormData` first.- **Validation:** only add a schema library when we actually need runtime validation at boundaries.- **UI components:** shadcn primitives via `pnpm dlx shadcn@latest add <name>`. Don't hand-roll what shadcn already ships.
Before adding a package, check:
1. Is there a native browser or TS/JS API that does this?2. Does shadcn/ui already cover it?3. Is it small, well-maintained, and worth the maintenance cost?
If yes to (3), add it — but flag the decision in the commit message.
## Layout (to be created during build)
```frontend/├── src/│ ├── components/ # App components. shadcn primitives under components/ui/│ ├── lib/ # Framework-agnostic helpers (http, api, auth, supabase, env)│ ├── pages/ # Route-level components│ ├── App.tsx # Router│ ├── main.tsx│ └── index.css # Tailwind directives + global theme tokens├── index.html├── vite.config.ts├── tsconfig.json└── package.json```
Keep imports consistent with the `@/*` alias (e.g. `@/lib/api`, `@/components/ui/button`).
## Code style (frontend-specific)
- **TypeScript strict.** No `any` unless there's no alternative; prefer `unknown` and narrow.- **Small, composable functions and components** over clever abstractions. Three similar lines > a premature generic.- **One component = one file.** Components stay small enough to fit on one screen.- **Tailwind classes inline.** No CSS modules, styled-components, Emotion, or `.module.css` for component styles. Global tokens live in `src/index.css`.
## Configuration
- All env reads go through a single `src/lib/env.ts` module that validates required vars at boot. Never read `import.meta.env.X` directly in components.- Env vars are prefixed `VITE_` (Vite convention). Anything not prefixed is not exposed to the client.
## Backend integration
- Talks to a separate Python backend over JSON. URL comes from `VITE_API_BASE_URL`.- Always use `api.get/post/put/patch/delete` from `@/lib/api` — it handles base URL, JSON, Supabase bearer token, timeouts, and typed `ApiError`s (including the `isNetworkError` flag that distinguishes CORS/network from HTTP errors).- Auth is Supabase email. The bearer token is injected automatically via the `api` client; never thread tokens through component props.
## Testing
**No frontend tests.** Do not write `*.test.ts` / `*.test.tsx` files or introduce a test runner. We verify the frontend manually in the browser plus `pnpm tsc --noEmit` and `pnpm lint`. If you find yourself reaching for vitest, Playwright, or Cypress — stop. That's not what this project does. Correctness for shared logic comes from keeping it simple and well-typed, not from a test suite.
## Anti-patterns (rejected)
- Reading `import.meta.env.X` directly outside `lib/env.ts`.- Importing an HTTP library when `fetch` would do.- Mixing client state libraries (Zustand + Jotai + Redux) for one project.- `any` annotations to silence the type-checker.- Custom CSS files / styled-components alongside Tailwind.- Re-implementing a shadcn primitive by hand.- Reaching for Next.js, SSR, or any framework that requires a Node server in front of the SPA.This article was written by Pi (Qwopus3.6-27B-NVFP4 | pc-eyay), based on content from: https://www.youtube.com/watch?v=qF5il_9IwME


