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>
8.7 KiB
8.7 KiB
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.tsxinside kebab-case directory (sign-in/page.tsx) - Layouts:
layout.tsx - Utilities: camelCase (
auth-server.ts,utils.ts) - Constants:
UPPER_SNAKE_CASEfor values, camelCase for files (constants.ts)
Imports
// 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)
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)
'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
const result = await authClient.changePassword({
currentPassword: values.currentPassword,
newPassword: values.newPassword,
revokeOtherSessions: true,
});
i18n Patterns
Server Component
import { getTranslations } from 'next-intl/server';
export default async function Page() {
const t = await getTranslations('Namespace');
return <h1>{t('Title')}</h1>;
}
Client Component
'use client';
import { useTranslations } from 'next-intl';
export function Component() {
const t = useTranslations('Namespace');
return <h1>{t('Title')}</h1>;
}
Adding Translations
- Add keys to
messages/en.jsonANDmessages/pl.json - Use namespace grouping (
AuthPage,DashboardPage,SettingsPage) - Never hardcode user-facing strings in components
Route Constants
Always use src/lib/routes.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:
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
useTranslationsin async server components — usegetTranslationsinstead - ❌ Never leave a private route unprotected — always use
isAuthenticated()+ redirect - ❌ Never hardcode
/sign-inor/dashboard— useroutes.public.signIn/routes.private.dashboard
i18n
- ❌ Never read
Accept-Languagemanually — next-intl middleware handles locale detection - ❌ Never hardcode
lang="en"— read fromx-next-intl-localeheader - ❌ Never forget to add both
en.jsonandpl.json— keep translations in sync
Code Quality
- ❌ Never use
as any— proper typing exists (seeconvex/betterAuth/auth.ts) - ❌ Never use
@/src/components/— use@/components/directly - ❌ Never mix
@ts-ignoreor@ts-expect-error— fix the type error instead - ❌ Never read
process.envdirectly — always use validatedenvfromsrc/lib/env.ts
Next.js 16 Specifics
- ❌ Never use
middleware.ts— Next.js 16 usesproxy.tsinstead - ❌ Never use Turbopack — it's broken in 16.2.1 (900% CPU spike). Use
pnpm dev --webpack
Adding a New Feature
New Protected Page
- Create directory under
src/app/[locale]/my-page/ - Add
page.tsxas async server component - Call
isAuthenticated()at the top - Redirect to
routes.public.signInwithcallbackURLif unauthenticated - Add route to
src/lib/routes.tsif it's a major page - Add translations to
messages/en.jsonandmessages/pl.json
New UI Component
- Check if shadcn/ui primitive exists first (
src/components/ui/) - If not, create in
src/components/my-feature/MyComponent.tsx - Use existing patterns:
cn()for class merging,Field/FieldGroupfor forms - Export from barrel file if you create an index
New Convex Function
- Create in
convex/myFeature.ts - Use
query({ args: {}, handler: async (ctx) => { ... } }) - Export and use via generated API
- 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 — check
node_modules/next/dist/docs/for exact APIs - Convex Docs
- Better Auth Docs
- next-intl Docs