t-convex-nextjs-saas/AGENTS.md
nxtkofi 8ff2246ecf 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>
2026-04-21 21:08:56 +02:00

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.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

// 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

  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:

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 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