t-convex-nextjs-saas/AGENTS.md

270 lines
8.7 KiB
Markdown
Raw Normal View History

2026-03-25 22:05:06 +00:00
<!-- BEGIN:nextjs-agent-rules -->
# 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.
<!-- 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/)