519 lines
14 KiB
Markdown
519 lines
14 KiB
Markdown
# 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.
|