docs: rewrite AGENTS.md with full architecture and conventions
Add comprehensive agent instructions covering architecture, directory structure, auth patterns, i18n patterns, route constants, environment variables, and anti-patterns. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
65e91fc6f6
commit
8ff2246ecf
1 changed files with 264 additions and 0 deletions
264
AGENTS.md
264
AGENTS.md
|
|
@ -3,3 +3,267 @@
|
||||||
|
|
||||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||||
<!-- END:nextjs-agent-rules -->
|
<!-- END:nextjs-agent-rules -->
|
||||||
|
|
||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
This is a **Next.js 16 + Convex (self-hosted) + Better Auth** SaaS template.
|
||||||
|
|
||||||
|
- **Frontend**: Next.js 16 App Router, React 19, TypeScript 5, Tailwind CSS 4
|
||||||
|
- **Backend**: Convex self-hosted (Coolify) with Better Auth for authentication
|
||||||
|
- **UI**: shadcn/ui (radix-nova style), @hugeicons/react icons
|
||||||
|
- **i18n**: next-intl v4 with locale-based routing (`/en`, `/pl`)
|
||||||
|
- **State**: React Server Components + Client Components hybrid. No global state library.
|
||||||
|
- **Auth**: Better Auth with email/password, HIBP plugin, Convex adapter
|
||||||
|
|
||||||
|
### Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/app/[locale]/layout.tsx` | Locale layout — validates locale, enables static rendering |
|
||||||
|
| `src/app/layout.tsx` | Root layout — theme provider, i18n provider, dynamic `lang` |
|
||||||
|
| `src/lib/auth-server.ts` | Server-side auth helpers (`isAuthenticated`, `getToken`) |
|
||||||
|
| `src/lib/auth-client.ts` | Client-side auth client (`authClient.signIn.email(...)`) |
|
||||||
|
| `src/i18n/routing.ts` | next-intl routing config (locales, defaultLocale, localePrefix) |
|
||||||
|
| `src/i18n/request.ts` | Request config — reads locale from middleware, loads messages |
|
||||||
|
| `src/proxy.ts` | next-intl middleware (Next.js 16 convention — was `middleware.ts`) |
|
||||||
|
| `convex/auth.ts` | Better Auth configuration (plugins, password policy, HIBP) |
|
||||||
|
| `convex/convex.config.ts` | Convex app definition |
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
app/[locale]/ # All pages live under locale segment
|
||||||
|
layout.tsx # Locale validation + setRequestLocale
|
||||||
|
page.tsx # Home page
|
||||||
|
dashboard/page.tsx # Protected dashboard (server page + isAuthenticated)
|
||||||
|
settings/page.tsx # Protected settings (server page + isAuthenticated)
|
||||||
|
sign-in/page.tsx # Auth form with callbackURL support
|
||||||
|
sign-up/page.tsx # Auth form with callbackURL support
|
||||||
|
api/auth/[...all]/ # Better Auth API route
|
||||||
|
components/
|
||||||
|
ui/ # shadcn/ui primitives (button, card, field, etc.)
|
||||||
|
auth/ # Auth-specific components
|
||||||
|
settings/ # Settings-specific components
|
||||||
|
core/ # App-wide components (ThemeChanger)
|
||||||
|
lib/
|
||||||
|
auth-server.ts # Server auth helpers
|
||||||
|
auth-client.ts # Client auth client
|
||||||
|
routes.ts # Route constants
|
||||||
|
utils.ts # cn() and utilities
|
||||||
|
env.ts # Validated environment variables (Zod)
|
||||||
|
i18n/
|
||||||
|
routing.ts # next-intl routing config
|
||||||
|
request.ts # next-intl request config
|
||||||
|
convex/
|
||||||
|
auth.ts # Better Auth setup
|
||||||
|
auth.config.ts # Convex auth config provider
|
||||||
|
convex.config.ts # Convex app definition
|
||||||
|
http.ts # Convex HTTP actions
|
||||||
|
betterAuth/ # Better Auth Convex component
|
||||||
|
messages/
|
||||||
|
en.json # English translations
|
||||||
|
pl.json # Polish translations
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
### File Naming
|
||||||
|
|
||||||
|
- **Components**: PascalCase (`PasswordChangeCard.tsx`, `AuthForm.tsx`)
|
||||||
|
- **Pages**: `page.tsx` inside kebab-case directory (`sign-in/page.tsx`)
|
||||||
|
- **Layouts**: `layout.tsx`
|
||||||
|
- **Utilities**: camelCase (`auth-server.ts`, `utils.ts`)
|
||||||
|
- **Constants**: `UPPER_SNAKE_CASE` for values, camelCase for files (`constants.ts`)
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Good
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { routes } from '@/lib/routes';
|
||||||
|
|
||||||
|
// Bad — never use @/src/components/
|
||||||
|
import { Card } from '@/src/components/ui/card';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Patterns
|
||||||
|
|
||||||
|
#### Server Component (check auth)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { isAuthenticated } from '@/lib/auth-server';
|
||||||
|
import { routes } from '@/lib/routes';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
export default async function ProtectedPage() {
|
||||||
|
const authenticated = await isAuthenticated();
|
||||||
|
if (!authenticated) {
|
||||||
|
const searchParams = new URLSearchParams({ callbackURL: '/dashboard' });
|
||||||
|
redirect(`${routes.public.signIn}?${searchParams.toString()}`);
|
||||||
|
}
|
||||||
|
// ... render protected content
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Client Component (auth actions)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
import { authClient } from '@/lib/auth-client';
|
||||||
|
|
||||||
|
async function handleSignIn(email: string, password: string) {
|
||||||
|
const result = await authClient.signIn.email({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
callbackURL: '/dashboard',
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
toast.error(result.error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Password Change
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const result = await authClient.changePassword({
|
||||||
|
currentPassword: values.currentPassword,
|
||||||
|
newPassword: values.newPassword,
|
||||||
|
revokeOtherSessions: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### i18n Patterns
|
||||||
|
|
||||||
|
#### Server Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const t = await getTranslations('Namespace');
|
||||||
|
return <h1>{t('Title')}</h1>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Client Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
export function Component() {
|
||||||
|
const t = useTranslations('Namespace');
|
||||||
|
return <h1>{t('Title')}</h1>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Adding Translations
|
||||||
|
|
||||||
|
1. Add keys to `messages/en.json` AND `messages/pl.json`
|
||||||
|
2. Use namespace grouping (`AuthPage`, `DashboardPage`, `SettingsPage`)
|
||||||
|
3. Never hardcode user-facing strings in components
|
||||||
|
|
||||||
|
### Route Constants
|
||||||
|
|
||||||
|
Always use `src/lib/routes.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { routes } from '@/lib/routes';
|
||||||
|
|
||||||
|
// Good
|
||||||
|
<Link href={routes.private.settings}>Settings</Link>
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
<Link href="/settings">Settings</Link>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
All env vars are validated at runtime in `src/lib/env.ts`. **Never read `process.env` directly** — import `env` instead:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { env } from '@/lib/env';
|
||||||
|
|
||||||
|
// Good
|
||||||
|
const url = env.NEXT_PUBLIC_CONVEX_URL;
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
const url = process.env.NEXT_PUBLIC_CONVEX_URL;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-Patterns (NEVER DO)
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
- ❌ **Never create custom Convex mutations for auth** — use Better Auth client API
|
||||||
|
- ❌ **Never use `useTranslations` in async server components** — use `getTranslations` instead
|
||||||
|
- ❌ **Never leave a private route unprotected** — always use `isAuthenticated()` + redirect
|
||||||
|
- ❌ **Never hardcode `/sign-in` or `/dashboard`** — use `routes.public.signIn` / `routes.private.dashboard`
|
||||||
|
|
||||||
|
### i18n
|
||||||
|
|
||||||
|
- ❌ **Never read `Accept-Language` manually** — next-intl middleware handles locale detection
|
||||||
|
- ❌ **Never hardcode `lang="en"`** — read from `x-next-intl-locale` header
|
||||||
|
- ❌ **Never forget to add both `en.json` and `pl.json`** — keep translations in sync
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- ❌ **Never use `as any`** — proper typing exists (see `convex/betterAuth/auth.ts`)
|
||||||
|
- ❌ **Never use `@/src/components/`** — use `@/components/` directly
|
||||||
|
- ❌ **Never mix `@ts-ignore` or `@ts-expect-error`** — fix the type error instead
|
||||||
|
- ❌ **Never read `process.env` directly** — always use validated `env` from `src/lib/env.ts`
|
||||||
|
|
||||||
|
### Next.js 16 Specifics
|
||||||
|
|
||||||
|
- ❌ **Never use `middleware.ts`** — Next.js 16 uses `proxy.ts` instead
|
||||||
|
- ❌ **Never use Turbopack** — it's broken in 16.2.1 (900% CPU spike). Use `pnpm dev --webpack`
|
||||||
|
|
||||||
|
## Adding a New Feature
|
||||||
|
|
||||||
|
### New Protected Page
|
||||||
|
|
||||||
|
1. Create directory under `src/app/[locale]/my-page/`
|
||||||
|
2. Add `page.tsx` as async server component
|
||||||
|
3. Call `isAuthenticated()` at the top
|
||||||
|
4. Redirect to `routes.public.signIn` with `callbackURL` if unauthenticated
|
||||||
|
5. Add route to `src/lib/routes.ts` if it's a major page
|
||||||
|
6. Add translations to `messages/en.json` and `messages/pl.json`
|
||||||
|
|
||||||
|
### New UI Component
|
||||||
|
|
||||||
|
1. Check if shadcn/ui primitive exists first (`src/components/ui/`)
|
||||||
|
2. If not, create in `src/components/my-feature/MyComponent.tsx`
|
||||||
|
3. Use existing patterns: `cn()` for class merging, `Field`/`FieldGroup` for forms
|
||||||
|
4. Export from barrel file if you create an index
|
||||||
|
|
||||||
|
### New Convex Function
|
||||||
|
|
||||||
|
1. Create in `convex/myFeature.ts`
|
||||||
|
2. Use `query({ args: {}, handler: async (ctx) => { ... } })`
|
||||||
|
3. Export and use via generated API
|
||||||
|
4. Never put auth logic in Convex — Better Auth owns that
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### `missing field functions` on `npx convex dev`
|
||||||
|
|
||||||
|
CLI and Convex backend image versions don't match. Upgrade the lower one.
|
||||||
|
|
||||||
|
### Locale not switching
|
||||||
|
|
||||||
|
Check `src/proxy.ts` matcher includes the route. Check browser has `NEXT_LOCALE` cookie.
|
||||||
|
|
||||||
|
### Auth redirect loop
|
||||||
|
|
||||||
|
Ensure `callbackURL` starts with `/`. Ensure `routes.public.signIn` doesn't itself require auth.
|
||||||
|
|
||||||
|
## External References
|
||||||
|
|
||||||
|
- [Next.js 16 Docs](https://nextjs.org/docs) — check `node_modules/next/dist/docs/` for exact APIs
|
||||||
|
- [Convex Docs](https://docs.convex.dev/)
|
||||||
|
- [Better Auth Docs](https://www.better-auth.com/)
|
||||||
|
- [next-intl Docs](https://next-intl.dev/)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue