14 KiB
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, andmemberships productshas been migrated to Route Handlers + Drizzleusershas been migrated to Route Handlers + Drizzle- RBAC now uses a global
users.system_roleplus organization-scopedmemberships.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
organizationsandmembershipsfor 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:
usersorganizationsmemberships
Important files:
src/auth.ts- Auth.js config and exported helperssrc/lib/auth/session.ts- shared server-side auth helperssrc/proxy.ts- route protectionsrc/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_rolewithsuper_admin | user - organization RBAC uses
memberships.rolewithadmin | 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
/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.mdplans/implementation-plan.mddocs/themes.mddocs/nav-rbac.mddocs/clerk_setup.mdis legacy historical reference only
Build & Development Commands
# 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
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
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:dbon 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
interfacefor object contracts - use
@/*imports fromsrc
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
useAppFormfrom@/components/ui/tanstack-form - use Zod for submit validation
- avoid
useStateinsideAppFieldrender props
Buttons
- use
<Button isLoading={isPending}>for loading states
Navigation & RBAC
Navigation is configured in src/config/nav-config.ts.
Current access properties:
systemRolerequireOrgpermissionroleplanfeature
Notes:
systemRoleis for global access such assuper_adminrequireOrgremains a compatibility seam and means the user must have an active organization- nav filtering in
src/hooks/use-nav.tsuses 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:
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:
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:
- define query options in
queries.ts - prefetch in the server page
- hydrate with
HydrationBoundary - read with
useSuspenseQuery()
Required pattern for CRUD mutations:
- every feature must define centralized query key factories in
queries.ts - use grouped keys such as
all,lists(),list(filters),details(),detail(id), plus child-resource keys likecontacts(id)orfollowups(id)when needed - after create, explicitly invalidate the list-level key
- after update, explicitly invalidate both list and detail keys
- after delete, explicitly invalidate the list key and remove stale detail queries with
removeQueries - invalidate related queries affected by the mutation, such as counts, child tabs, document previews, approval panels, and cross-feature related lists
- do not rely on
staleTimealone to refresh production CRUD data - do not rely on
router.refresh()alone for CRUD freshness; React Query invalidation is required first, androuter.refresh()is optional only as a supplement
Mutation callback safety rule:
- if a shared mutation object from
api/mutations.tsis spread intouseMutation({ ...sharedMutation, ... }), do not overrideonSuccessoronSettledin 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
onSettledand keep UI concerns like toast and closing dialogs in componentonSuccess, or - call the shared invalidate helper explicitly from the component before closing the sheet/dialog
- put invalidation in shared
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:
productsis migrated to Auth.js + Route Handlers + Drizzleusersis migrated to Auth.js + Route Handlers + Drizzle- new features should follow the
productsanduserspattern, not the old mock pattern
Legacy seams still present:
src/constants/mock-api.tssrc/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_SECRETDATABASE_URLSUPER_ADMIN_EMAILSUPER_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
accessinnav-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
- Auth.js
- Drizzle ORM
- shadcn/ui
- Tailwind CSS v4
- TanStack Query
- TanStack Table
- TanStack Form
- Sentry Next.js
Notes for AI Agents
- Always use
cn()for className merging. - Respect the feature-based structure and keep new work inside
src/features/. - Server components are the default.
- Avoid
any; keep contracts explicit. - Use
PageContainerfor page headers instead of importingHeadingdirectly in pages. - Use TanStack Form + Zod via the existing project wrappers.
- Use icons only through
@/components/icons. - For new app-owned features, prefer Route Handlers + Drizzle + React Query.
- Never import from
@/constants/mock-api*directly in components. - Use
@/authandsrc/lib/auth/session.tsfor auth and organization checks. - Do not introduce new Clerk usage.
- Treat legacy mock-backed modules as migration seams.
- Follow the migrated
productsandusersfeature patterns when building new CRUD modules. - Do not re-enable self-service sign-up unless the product requirement changes explicitly.
- For every CRUD mutation, explicitly invalidate React Query caches for list/detail/related data; never rely on passive staleness recovery.
- Centralize query keys in each feature's
api/queries.ts; do not scatter manual string query keys in components. - When deleting entities, remove stale detail queries with
removeQueriesin addition to invalidating lists. - If a component overrides
useMutationcallbacks on top of a shared mutation config, preserve or explicitly call the shared invalidation behavior before closing UI. - 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.