diff --git a/AGENTS.md b/AGENTS.md index 8bd0e39..84494f3 100644 --- a/AGENTS.md +++ b/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. + +# 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

{t('Title')}

; +} +``` + +#### Client Component + +```tsx +'use client'; +import { useTranslations } from 'next-intl'; + +export function Component() { + const t = useTranslations('Namespace'); + return

{t('Title')}

; +} +``` + +#### 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 +Settings + +// Bad +Settings +``` + +### 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/)