For a single-app Next.js SaaS on the App Router with TypeScript, Supabase for auth and database, Stripe for subscriptions, and Tailwind for styling. It assumes server components by default, Drizzle or the Supabase client for data access, and deployment on Vercel. Adjust the paths if you keep routes outside src/app.
# AGENTS.md
This is a subscription SaaS built with Next.js (App Router), Supabase, and Stripe.
## Project
Stack: Next.js 15 (App Router), TypeScript (strict), Tailwind, Supabase (auth + Postgres), Stripe, deployed on Vercel.
Layout:
- `src/app/(marketing)/` public pages, static where possible
- `src/app/(app)/` authenticated product, every route under an auth guard
- `src/app/api/` route handlers, including `src/app/api/stripe/webhook/route.ts`
- `src/components/ui/` primitives, `src/components/` composed features
- `src/lib/supabase/server.ts` server client, `src/lib/supabase/client.ts` browser client
- `src/lib/stripe.ts` Stripe SDK instance and price ID map
- `src/db/schema.ts` Drizzle schema, `src/db/migrations/` generated SQL
- `middleware.ts` session refresh and route protection
Entry points: `src/app/layout.tsx` (root shell), `middleware.ts` (auth), `src/app/api/stripe/webhook/route.ts` (billing state).
## Commands
Use pnpm. Do not use npm or yarn, the lockfile is pnpm-lock.yaml.
```bash
pnpm install # install dependencies
pnpm dev # local dev server on :3000
pnpm build # production build, must pass before any PR
pnpm start # serve the production build
pnpm lint # eslint
pnpm typecheck # tsc --noEmit
pnpm test # vitest run
pnpm test src/lib/billing # run one file or directory
pnpm db:generate # generate a migration from schema.ts
pnpm db:migrate # apply migrations to the current DATABASE_URL
pnpm db:studio # inspect data locally
```
Stripe webhooks locally: `stripe listen --forward-to localhost:3000/api/stripe/webhook`.
Before returning work, run `pnpm typecheck` and `pnpm lint`.
## Code style
- Server components are the default. Add `"use client"` only for state, effects, or browser APIs, and push it as far down the tree as possible.
- Never query the database or read secrets in a client component.
- Data fetching happens in server components or route handlers, never in useEffect.
- Validate every request body and form input with Zod before it reaches the database.
- Use the typed Supabase client from `src/lib/supabase/server.ts`. Do not construct clients inline.
- Absolute imports only, via the `@/` alias. No `../../` chains.
- No `any`. If a type is genuinely unknown use `unknown` and narrow it.
- Tailwind utilities in the markup. No CSS modules, no styled-components, no inline style objects for layout.
- Money is stored and passed around in integer cents, never floats.
- User facing strings live in the component, not in helper files.
## Boundaries
Do not touch without explicit instruction:
- `src/db/migrations/` Migrations are generated, never hand edited, and never deleted once applied.
- `pnpm-lock.yaml` Do not regenerate. Add dependencies with `pnpm add` so the diff stays minimal.
- `.env`, `.env.local`, or any secret value. Add new keys to `.env.example` with an empty value instead.
- `.github/workflows/` and `vercel.json`.
- Stripe price IDs and product configuration.
Needs human review before merge: anything under `src/app/api/stripe/`, changes to `middleware.ts`, row level security policies, and any change to how a subscription tier maps to product access.
If a task appears to require a schema change, propose the schema diff first and wait.
## Testing
- `pnpm test` runs the suite with Vitest. `pnpm test <path>` runs one file.
- Required tests: billing logic (plan resolution, proration, webhook handlers), auth guards, and any pure function in `src/lib/`.
- Webhook handlers are tested against recorded Stripe event fixtures in `src/test/fixtures/stripe/`. Add a fixture rather than mocking the SDK.
- UI tests are not required for presentational components.
- A bug fix ships with a test that fails without the fix.
## Git workflow
- Branch from `main`: `feat/short-description`, `fix/short-description`, `chore/short-description`.
- Conventional commits: `feat(billing): handle subscription downgrade`. Imperative mood, lowercase subject, no trailing period.
- One logical change per commit. Do not mix a refactor with a behavior change.
- Never commit directly to `main` and never force push a shared branch.
- PR description states what changed, why, and how it was verified. Link the issue. Note any migration or env var the reviewer must run.
A Next.js SaaS has three layers an agent constantly confuses: server components, client components, and route handlers. Stating where each lives, and where auth and billing code sits, prevents an agent from putting a database query in a client component or duplicating Stripe logic. Name the entry points explicitly so file search is not a guess.
Package manager choice matters more here than in most stacks because a wrong npm install rewrites the lockfile and breaks the Vercel build. Listing the exact dev, build, typecheck, and migration commands means the agent verifies its own work instead of assuming it compiled. Include the database commands, since schema changes are the most common source of a broken local environment.
Most Next.js style problems are architectural, not cosmetic: unnecessary use client directives, fetching in the wrong place, unvalidated form input. Concrete, checkable rules give the agent something to self-review against before it hands work back. Leave formatting to Prettier and spend this section on the decisions a formatter cannot make.
Billing and auth are the two places where an autonomous edit can cost real money or leak data. Migrations and generated Supabase types are also easy for an agent to rewrite by hand and hard for a reviewer to catch. Spell out what requires a human so the agent stops and asks instead of guessing.
SaaS regressions cluster around checkout, webhooks, and access control, so those deserve mandatory coverage even when the rest of the app is untested. Telling the agent how to run a single test file keeps feedback loops short. Stripe webhooks in particular need a documented local flow or the agent will invent one.
Branch and commit conventions let the agent open a pull request that looks like everyone else on the team wrote it. Stating what belongs in a PR description saves the reviewer from reconstructing intent. It also sets the expectation that the agent never pushes to the default branch.
Claude Code looks for CLAUDE.md. Most other agents and editors read AGENTS.md. Rather than maintaining both and letting them drift, keep one real file and symlink the other:
ln -s AGENTS.md CLAUDE.md
git add AGENTS.md CLAUDE.md Git stores the symlink, so it survives cloning on macOS and Linux. On Windows it needs developer mode or a stub file that references the real one. The full comparison is in CLAUDE.md vs AGENTS.md.
SvelteKit App
SvelteKit app with server load functions, form actions, and Vitest.
Agency Multi-Client
Shared codebase serving several client projects with isolated config.
Turborepo Monorepo
Multi-package workspace with shared UI, config, and typed contracts.
Django SaaS
Django app with per-app structure, Celery jobs, and Stripe billing.