# AGENTS.md - AI Coding Agent Reference

This file provides project-specific guidance for AI coding agents working in this repository. It reflects the current architecture and migration status of the codebase.

---

## Project Overview

This project is a Next.js admin dashboard starter that is being migrated from template scaffolding to an app-owned architecture.

Current core stack:

- **Framework**: Next.js 16 (App Router)
- **Language**: TypeScript 5.7
- **UI**: shadcn/ui + Tailwind CSS v4
- **Authentication**: Auth.js
- **Persistence**: PostgreSQL + Drizzle ORM
- **Data Fetching**: TanStack React Query
- **URL State**: nuqs
- **Forms**: TanStack Form + Zod
- **Charts**: Recharts
- **Monitoring**: Sentry
- **Package Manager**: Bun preferred, npm acceptable

Current migration status:

- Auth has been moved to **Auth.js**
- Multi-tenant data now uses app-owned **`users`**, **`organizations`**, and **`memberships`**
- **`products`** has been migrated to Route Handlers + Drizzle
- **`users`** has been migrated to Route Handlers + Drizzle
- RBAC now uses a global `users.system_role` plus organization-scoped `memberships.role`
- self-service sign-up is disabled; accounts must be created by an admin
- some legacy pages still exist as placeholders or transitional seams

---

## Technology Stack Details

### Core Runtime

- Next.js 16.x with App Router
- React 19.x
- TypeScript strict mode

### Styling & UI

- Tailwind CSS v4
- shadcn/ui components
- CSS custom properties and theme registry

### State Management

- Zustand for local UI state
- nuqs for URL search params
- TanStack Form + Zod for forms

### Data Fetching

- TanStack React Query for server state
- server prefetch with `HydrationBoundary` + `dehydrate`
- `useSuspenseQuery()` for client consumption of prefetched data

### Authentication & Authorization

- Auth.js for authentication
- app-owned `organizations` and `memberships` for multi-tenant access
- global system role stored on `users.system_role`
- organization role and permissions stored on `memberships`
- client-side nav filtering is UX-only; real protection must be server-side

### Persistence & APIs

- PostgreSQL + Drizzle ORM
- Route Handlers under `src/app/api/**` are the main server boundary
- feature services call those route handlers through `src/lib/api-client.ts`
- mock APIs remain only as **legacy migration seams**

---

## Current Auth Model

The current auth model is app-owned and centered around:

- `users`
- `organizations`
- `memberships`

Important files:

- `src/auth.ts` - Auth.js config and exported helpers
- `src/lib/auth/session.ts` - shared server-side auth helpers
- `src/proxy.ts` - route protection
- `src/db/schema.ts` - current database schema

Current behavior:

- authentication uses an Auth.js **Credentials** provider
- passwords are verified against `users.password_hash`
- global RBAC uses `users.system_role` with `super_admin | user`
- organization RBAC uses `memberships.role` with `admin | user`
- the user row stores `active_organization_id`
- session enrichment loads organization and membership data on each request
- self-service sign-up is disabled
- initial super admin bootstrap is done with the seed script, not through the UI

Preferred server-side helpers:

- `requireSession()`
- `requireSystemRole()`
- `requireOrganizationAccess(options?)`

Do not introduce new Clerk usage.

---

## Project Structure

```text
/src
├── app/                    # Next.js App Router
│   ├── auth/               # Auth pages
│   ├── dashboard/          # Dashboard routes
│   ├── api/                # Route handlers
│   ├── layout.tsx          # Root layout
│   └── page.tsx            # Landing / redirect entry
│
├── components/
│   ├── ui/                 # shadcn/ui and app UI primitives
│   ├── layout/             # Sidebar, header, providers
│   ├── forms/              # Form field wrappers
│   ├── themes/             # Theme system
│   └── icons.tsx           # Single icon registry
│
├── db/
│   └── schema.ts           # Drizzle schema
│
├── features/
│   ├── auth/
│   ├── overview/
│   ├── products/           # Migrated: React Query + Route Handlers + Drizzle
│   ├── users/              # Migrated: React Query + Route Handlers + Drizzle + RBAC
│   ├── react-query-demo/
│   ├── kanban/
│   ├── chat/
│   ├── notifications/
│   └── profile/
│
├── config/
│   └── nav-config.ts
│
├── hooks/
│   ├── use-nav.ts
│   └── use-data-table.ts
│
├── lib/
│   ├── api-client.ts
│   ├── auth/
│   ├── db.ts
│   ├── query-client.ts
│   └── searchparams.ts
│
├── styles/
└── types/
```

Relevant docs/plans:

- `plans/req.md`
- `plans/implementation-plan.md`
- `docs/themes.md`
- `docs/nav-rbac.md`
- `docs/clerk_setup.md` is legacy historical reference only

---

## Build & Development Commands

```bash
# Install dependencies
bun install
# or
npm install

# Development server
bun run dev
# or
npm run dev

# Build
bun run build
# or
npm run build

# Start
bun run start
# or
npm run start

# Lint
bun run lint
# or
npm run lint

# Format
bun run format
# or
npm run format
```

---

## Environment Configuration

Copy `env.example.txt` to `.env.local` and configure:

### Required

```env
AUTH_SECRET=your-long-random-secret
DATABASE_URL=postgres://postgres:postgres@localhost:5432/training_system
SUPER_ADMIN_EMAIL=admin@example.com
SUPER_ADMIN_PASSWORD=change-this-password
```

### Optional for Sentry

```env
NEXT_PUBLIC_SENTRY_DSN=https://...@....ingest.sentry.io/...
NEXT_PUBLIC_SENTRY_ORG=your-org
NEXT_PUBLIC_SENTRY_PROJECT=your-project
SENTRY_AUTH_TOKEN=sntrys_...
NEXT_PUBLIC_SENTRY_DISABLED="false"
```

Notes:

- keep auth and database secrets server-only
- OAuth providers can be added later through Auth.js
- billing is currently modeled as app-owned organization plan data, not a live provider integration
- run `npm run setup:db` on first setup to apply migrations and seed the initial super admin

---

## Code Style Guidelines

### TypeScript

- strict mode enabled
- use explicit return types for public functions when useful
- prefer `interface` for object contracts
- use `@/*` imports from `src`

### Component Conventions

- use function declarations for components
- server components by default
- add `'use client'` only when needed
- use `cn()` for className merging

### Forms

- use TanStack Form via `useAppForm` from `@/components/ui/tanstack-form`
- use Zod for submit validation
- avoid `useState` inside `AppField` render props

### Buttons

- use `<Button isLoading={isPending}>` for loading states

---

## Navigation & RBAC

Navigation is configured in `src/config/nav-config.ts`.

Current access properties:

- `systemRole`
- `requireOrg`
- `permission`
- `role`
- `plan`
- `feature`

Notes:

- `systemRole` is for global access such as `super_admin`
- `requireOrg` remains a compatibility seam and means the user must have an active organization
- nav filtering in `src/hooks/use-nav.ts` uses Auth.js session data
- server routes and server pages must enforce real access independently

---

## Authentication Patterns

### Protected Routes

Dashboard routes are protected through `src/proxy.ts`.

Example protected page:

```tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export default async function Page() {
  const session = await auth();

  if (!session?.user) {
    redirect('/auth/sign-in');
  }

  if (!session.user.activeOrganizationId) {
    redirect('/dashboard/workspaces');
  }

  return <div>Protected</div>;
}
```

### Shared Server Helpers

Prefer:

- `requireSession()`
- `requireSystemRole()`
- `requireOrganizationAccess(options?)`

### Plan / Feature Gating

Do not use vendor-specific gating components.

Read plan or feature state from app-owned organization data and enforce it server-side.

---

## Data Fetching Patterns

### Service Layer Architecture

Each feature should use:

```text
src/features/<name>/api/
  types.ts
  service.ts
  queries.ts
  mutations.ts   # when mutations exist
```

Rules:

- components import types from `types.ts`
- components call query options from `queries.ts`
- services call local route handlers through `apiClient`
- route handlers talk to Drizzle / database

### Backend Patterns

Preferred patterns in this repo:

| Pattern | Guidance |
| --- | --- |
| Route Handlers + ORM | Preferred for app-owned features |
| Server Actions + ORM | Acceptable when it fits the feature well |
| BFF | Acceptable when proxying to an external backend |
| Mock | Legacy only, not the preferred path |

### React Query

Preferred pattern for new pages:

1. define query options in `queries.ts`
2. prefetch in the server page
3. hydrate with `HydrationBoundary`
4. read with `useSuspenseQuery()`

Required pattern for CRUD mutations:

1. every feature must define centralized query key factories in `queries.ts`
2. use grouped keys such as `all`, `lists()`, `list(filters)`, `details()`, `detail(id)`, plus child-resource keys like `contacts(id)` or `followups(id)` when needed
3. after create, explicitly invalidate the list-level key
4. after update, explicitly invalidate both list and detail keys
5. after delete, explicitly invalidate the list key and remove stale detail queries with `removeQueries`
6. invalidate related queries affected by the mutation, such as counts, child tabs, document previews, approval panels, and cross-feature related lists
7. do not rely on `staleTime` alone to refresh production CRUD data
8. do not rely on `router.refresh()` alone for CRUD freshness; React Query invalidation is required first, and `router.refresh()` is optional only as a supplement

Mutation callback safety rule:

- if a shared mutation object from `api/mutations.ts` is spread into `useMutation({ ...sharedMutation, ... })`, do not override `onSuccess` or `onSettled` in a way that drops cache invalidation
- prefer keeping cache invalidation inside the shared mutation definition
- if component-level success behavior is needed, either:
  - put invalidation in shared `onSettled` and keep UI concerns like toast and closing dialogs in component `onSuccess`, or
  - call the shared invalidate helper explicitly from the component before closing the sheet/dialog

UI completion rule after successful mutation:

- close sheet/dialog only after the mutation promise resolves successfully
- table/detail views must show fresh data immediately without a full browser refresh
- related tabs and counts must refresh in the same interaction when their backing data changed

### URL State

Use `nuqs` for table search params and list filters.

### Tables

Use:

- TanStack Table
- React Query
- `useDataTable`
- column definitions under the feature component tree

---

## Current Migration Guidance

Use the migrated `products` feature as the main reference for new CRUD work.

Important current status:

- `products` is migrated to Auth.js + Route Handlers + Drizzle
- `users` is migrated to Auth.js + Route Handlers + Drizzle
- new features should follow the `products` and `users` pattern, not the old mock pattern

Legacy seams still present:

- `src/constants/mock-api.ts`
- `src/constants/mock-api-users.ts`
- some placeholder pages under workspaces, billing, profile, and exclusive sections

Treat those as migration seams, not target architecture.

---

## Deployment Notes

### Production Requirements

Ensure these are set:

- `AUTH_SECRET`
- `DATABASE_URL`
- `SUPER_ADMIN_EMAIL`
- `SUPER_ADMIN_PASSWORD`
- relevant `SENTRY_*` variables if using Sentry

### Build Considerations

- app builds as standalone
- database migrations must be applied before runtime
- production secrets must not be exposed as `NEXT_PUBLIC_*`

---

## Troubleshooting

### Auth fails in development

- verify `AUTH_SECRET`
- verify `DATABASE_URL`
- verify migrations are applied
- verify the super admin seed has run and seeded users have valid `password_hash`

### Theme not applying

- verify theme CSS import in `src/styles/theme.css`
- verify theme registration in `theme.config.ts`

### Navigation items not showing

- check `access` in `nav-config.ts`
- verify session has active organization, role, or permissions as needed

### Feature data looks stale

- verify mutation invalidates the feature query key factory
- verify filters are included in the query key

---

## External Documentation

- [Next.js App Router](https://nextjs.org/docs/app)
- [Auth.js](https://authjs.dev)
- [Drizzle ORM](https://orm.drizzle.team)
- [shadcn/ui](https://ui.shadcn.com/docs)
- [Tailwind CSS v4](https://tailwindcss.com/docs)
- [TanStack Query](https://tanstack.com/query)
- [TanStack Table](https://tanstack.com/table/latest)
- [TanStack Form](https://tanstack.com/form)
- [Sentry Next.js](https://docs.sentry.io/platforms/javascript/guides/nextjs/)

---

## Notes for AI Agents

1. Always use `cn()` for className merging.
2. Respect the feature-based structure and keep new work inside `src/features/`.
3. Server components are the default.
4. Avoid `any`; keep contracts explicit.
5. Use `PageContainer` for page headers instead of importing `Heading` directly in pages.
6. Use TanStack Form + Zod via the existing project wrappers.
7. Use icons only through `@/components/icons`.
8. For new app-owned features, prefer Route Handlers + Drizzle + React Query.
9. Never import from `@/constants/mock-api*` directly in components.
10. Use `@/auth` and `src/lib/auth/session.ts` for auth and organization checks.
11. Do not introduce new Clerk usage.
12. Treat legacy mock-backed modules as migration seams.
13. Follow the migrated `products` and `users` feature patterns when building new CRUD modules.
14. Do not re-enable self-service sign-up unless the product requirement changes explicitly.
15. For every CRUD mutation, explicitly invalidate React Query caches for list/detail/related data; never rely on passive staleness recovery.
16. Centralize query keys in each feature's `api/queries.ts`; do not scatter manual string query keys in components.
17. When deleting entities, remove stale detail queries with `removeQueries` in addition to invalidating lists.
18. If a component overrides `useMutation` callbacks on top of a shared mutation config, preserve or explicitly call the shared invalidation behavior before closing UI.
19. After successful create/update/delete, sheets, dialogs, and destructive modals must close only after cache invalidation has been triggered and the UI can refresh from fresh data.
