# This is NOT the Next.js you know 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/)