# Phase 2: Admin Area & Interactive Features - Research
**Researched:** 2026-05-15
**Domain:** Next.js authentication, admin CRUD, form handling, route protection
**Confidence:** MEDIUM-HIGH
## Summary
Phase 2 requires implementing Next.js 16 authentication for admin area (`/admin`), form-based CRUD operations for clients/phases/tasks/deliverables/payments, and client-facing interactive features (approval, comments). The research reveals a critical version decision: **next-auth v4 has unresolved peer dependency issues with Next.js 16**, requiring workarounds (`--legacy-peer-deps` flag). The CONTEXT.md decision to use "next-auth@4" is technically viable but carries technical debt. The phase architecture is sound: Edge middleware protection (proxy.ts) for admin routes, Server Actions for mutations, and API routes for client interactions (separate auth path). Key dependencies are not yet installed.
**Primary recommendation:** Proceed with next-auth v4 as locked in CONTEXT.md, but document the workaround requirement. All other patterns (Server Actions, revalidatePath, Zod+RHF forms, tabs) are stable and production-ready on Next.js 16.
## User Constraints (from CONTEXT.md)
### Locked Decisions
**D-01: Auth.js v4 (`next-auth@4`)** — versione stabile. Installare `next-auth@4` + credentials via env vars (ADMIN_EMAIL + ADMIN_PASSWORD). No DB table per utenti.
**D-02: Credenziali admin via env vars** — Singolo admin hardcoded in `.env.local`. CredentialsProvider di Auth.js valida contro le env vars.
**D-03: Sessione JWT** — Nessuna tabella session nel DB. JWT stateless firmato, scadenza 30 giorni.
**D-04: Protezione route admin** — `src/proxy.ts` gestisce `/c/[token]`. Aggiungere matcher `/admin/:path*` → redirect a `/admin/login` se no session. Check sessione nel middleware via `getToken` da next-auth/jwt.
**D-05: Server Actions** — Next.js 15/16 nativo. Nessuna API route separata per CRUD admin. Form → Server Action → Drizzle → `revalidatePath`.
**D-06: API route solo per client interactions** — Approvazioni e commenti del cliente su API routes (non Server Actions). Route pattern: `POST /api/client/approve` e `POST /api/client/comment`. Validate token via header/cookie, non Auth.js.
**D-07: Lista → dettaglio** — `/admin` → lista clienti con badge stato pagamenti. Click → `/admin/clients/[id]` dettaglio completo.
**D-08: Tabs per sezioni cliente** — Nella pagina dettaglio cliente, tabs: Panoramica | Fasi & Task | Documenti | Pagamenti | Commenti. Aggiungere `@radix-ui/react-tabs`.
**D-09: Sidebar assente in v1** — Nessuna sidebar globale. Nav minimal: logo + link "Clienti" + pulsante logout.
**D-10: Approvazione deliverable** — Pulsante "Approva" visibile solo se `approved_at` è null. POST a `/api/client/approve` con `{ deliverableId }`.
**D-11: Commenti inline** — Textarea + pulsante "Invia" sotto task/deliverable. POST a `/api/client/comment`. Lista commenti sopra il form, ordinati per `created_at` asc.
### Claude's Discretion
- Struttura esatta delle Server Actions (file naming)
- Ordine campi nei form admin
- Icone lucide-react per stati
- Colori badge admin
### Deferred Ideas (OUT OF SCOPE)
- Brand customization panel (Phase 3+)
- Sidebar globale (v1 non necessaria)
- Real-time commenti (WebSocket/polling → v2)
- Multi-admin / ruoli
- Password change UI
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| ADMIN-01 | Vista di tutti i clienti con stato sintetico | Server Component + Drizzle query per lista con badge pagamenti |
| ADMIN-02 | Gestione completa di ogni cliente: fasi, task, documenti, pagamenti | Server Actions per CRUD su tutte le tabelle; proxy.ts protezione route `/admin/[*]`; revalidatePath aggiorna UI |
| DASH-05 | Il cliente può approvare i deliverable dalla sua area | API route `POST /api/client/approve` — token validation — UPDATE `approved_at` immutabile |
| DASH-06 | Il cliente può lasciare commenti su task e deliverable | API route `POST /api/client/comment` — INSERT comments table — lista commenti con author (client/admin) |
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Admin authentication & session | API / Backend (proxy.ts) | Frontend Server (layout redirect) | JWT validation happens at Edge; session unavailable → redirect to login before rendering |
| Admin CRUD mutations | API / Backend (Server Actions) | Frontend Server (forms) | Server Actions run in Node context; Drizzle mutations; revalidatePath triggers UI refresh |
| Admin UI rendering | Frontend Server (Server Components) | Browser (Client Components for interactivity) | List/detail pages are Server Components with data from Drizzle; tabs/forms use Client Components for state |
| Client approval/comments | API / Backend (route handlers) | Browser (client tokens in URL/cookie) | Separate auth path from admin; token validation in route handlers; mutations persist to DB |
| Client-facing UI (approval, comments) | Browser / Client | Frontend Server (token validation via middleware) | Approval button, comment form are interactive client components; middleware validates token before rendering |
| Payment status display | API / Backend (Drizzle query) | Frontend Server (Server Component) | Payments table joins with clients; status badge computed server-side; client API returns `accepted_total` only |
## Standard Stack
### Core Authentication & Middleware
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| next-auth | 4.24.14 | JWT-based admin session, credentials provider | Industry standard for Next.js; stateless JWT avoids DB session lookups at Edge [VERIFIED: npm registry] |
| next-auth/jwt | bundled with next-auth | Token signing/verification in middleware | Required for getToken() calls in proxy.ts [VERIFIED: next-auth docs] |
### Form Handling & Validation
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| react-hook-form | 7.75.0 | Client-side form state + server submission | Already installed; lightweight; pairs with Zod for validation [VERIFIED: package.json] |
| @hookform/resolvers | 5.2.2 | Adapter for Zod → RHF integration | Already installed; official resolver [VERIFIED: package.json] |
| zod | 4.4.3 | Schema validation (client & server) | Already installed; enables validation reuse across RHF forms and Server Actions [VERIFIED: package.json] |
### UI Components
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| @radix-ui/react-tabs | 1.0.x | Tabs primitive (Radix-based) | Not yet installed; needed for admin detail page tabs per D-08 [CITED: shadcn/ui docs] |
| @radix-ui/react-dialog | 1.1.x | Modal/dialog primitive | Optional; can defer if confirm modals not needed in Wave 1 |
| shadcn/ui (tabs component) | — | Pre-configured Tabs built on Radix | Install via `npx shadcn@latest add tabs` after @radix-ui/react-tabs added [CITED: shadcn/ui] |
### Existing, Reusable Components (from Phase 1)
| Library | Version | Purpose | Status |
|---------|---------|---------|--------|
| next | 16.2.6 | App Router, Server Components, Server Actions | ✅ Installed, using in Phase 2 |
| drizzle-orm | 0.45.2 | Type-safe ORM + postgres-js connection | ✅ Installed, patterns established Phase 1 |
| postgres | 3.4.9 | Postgres client (Node.js only, not Edge-compatible) | ✅ Installed; used in API routes (not middleware) |
| nanoid | 5.1.11 | Cryptographic token generation | ✅ Installed; schema uses for `token` field |
| tailwindcss | 4.x | Styling | ✅ Installed v4 in Phase 1 |
| shadcn/ui (installed) | — | badge, button, card, input, label, select, table, textarea | ✅ All used in Phase 1; reuse for admin UI |
| lucide-react | 1.14.0 | Icons | ✅ Installed Phase 1; use for payment status icons |
### Installation
**New packages to add:**
```bash
npm install next-auth@4.24.14 @radix-ui/react-tabs
npx shadcn@latest add tabs
```
**Note:** next-auth v4 with Next.js 16 requires `--legacy-peer-deps` flag due to unresolved peer dependency issue:
```bash
npm install next-auth@4.24.14 --legacy-peer-deps
```
**Version verification:**
```bash
npm view next-auth@4.24.14 version # Should show: 4.24.14, published ~May 2026
npm view @radix-ui/react-tabs version # Should show: 1.x.x (latest stable)
```
## Architecture Patterns
### System Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ BROWSER / CLIENT │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Admin Form │ │ Client Dashboard │ │
│ │ (RHF + Zod) │ │ (Approval/Comment) │
│ └────────┬─────────┘ └────────┬─────────┘ │
└───────────┼──────────────────────┼───────────────────────────┘
│ │
│ POST /api/[...] + │ POST /api/client/[...] +
│ Server Action │ client token
│ │
┌───────────▼──────────────────────▼───────────────────────────┐
│ FRONTEND SERVER (Next.js App Router) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Route Handler: POST /api/client/approve │ │
│ │ Route Handler: POST /api/client/comment │ │
│ │ Server Actions: createClient, updatePhase, etc. │ │
│ └──────────────────────────────────────────────────────┘ │
└───────────┬──────────────────────────────────────────────────┘
│
│ Token validation,
│ Drizzle mutation
│
┌───────────▼──────────────────────────────────────────────────┐
│ DATABASE (Hetzner Postgres) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ clients, phases, tasks, deliverables, comments, │ │
│ │ payments, documents, notes, service_catalog, │ │
│ │ quote_items │ │
│ └──────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
SEPARATELY: Edge Layer (proxy.ts)
┌─────────────────────────────────────────────────────────────┐
│ EDGE (Vercel / Next.js middleware layer) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ proxy.ts: getToken() → session exists? │ │
│ │ → Route: /admin/[*] → getToken ok? → allow : 404 │ │
│ │ → Route: /c/[token]/[*] → validateToken ok? → allow │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Route Structure & Protection
**Admin Routes (Protected by Edge middleware):**
- `/admin/login` — GET form, POST credentials (unprotected)
- `/admin` — GET list of clients (protected, Server Component)
- `/admin/clients/[id]` — GET detail + CRUD forms (protected, Server Component + Client Components)
- `/admin/clients/[id]/phases` — Nested detail (protected)
- `/admin/clients/[id]/payments` — Nested detail (protected)
**Client Routes (Already protected in Phase 1):**
- `/c/[token]/` — Client dashboard (token validation via proxy.ts)
- `/c/[token]/task/[taskId]` — Task detail with comments
**API Routes (Internal, not middleware-protected at Edge):**
- `POST /api/client/approve` — Expects client token in body/header
- `POST /api/client/comment` — Expects client token in body/header
- `POST /api/internal/validate-token` — Called by proxy.ts (no auth needed, internal only)
### Recommended Project Structure
```
src/
├── app/
│ ├── admin/
│ │ ├── login/
│ │ │ ├── page.tsx # Login form
│ │ │ └── actions.ts # signIn Server Action
│ │ ├── clients/
│ │ │ ├── page.tsx # List all clients
│ │ │ ├── actions.ts # createClient, deleteClient
│ │ │ └── [id]/
│ │ │ ├── page.tsx # Detail page with tabs
│ │ │ ├── actions.ts # updateClient, createPhase, etc.
│ │ │ └── layout.tsx # Detail layout (optional nested)
│ │ ├── layout.tsx # Admin layout (nav, logout)
│ │ └── components/
│ │ ├── ClientForm.tsx
│ │ ├── PhaseForm.tsx
│ │ └── PaymentBadge.tsx
│ ├── c/
│ │ └── [token]/
│ │ ├── components/
│ │ │ ├── ApprovalButton.tsx
│ │ │ └── CommentForm.tsx
│ │ └── ... # Phase 1 routes (reuse)
│ └── api/
│ ├── client/
│ │ ├── approve/
│ │ │ └── route.ts # POST /api/client/approve
│ │ └── comment/
│ │ └── route.ts # POST /api/client/comment
│ └── internal/
│ └── validate-token/
│ └── route.ts # (existing, Phase 1)
├── auth.ts # next-auth config (NEW)
├── proxy.ts # Edge middleware (MODIFY)
├── db/
│ └── schema.ts # (unchanged, Phase 1)
└── lib/
└── client-view.ts # (unchanged, Phase 1)
```
### Pattern 1: Server Actions for Admin CRUD
**What:** Async functions marked with `'use server'` that run on the server, handle form submissions, mutate the database via Drizzle, and revalidate the cache to refresh the UI.
**When to use:** Every admin form (create/edit/delete client, phase, task, payment). Never call from client-side mutation code directly.
**Example:**
```typescript
// src/app/admin/clients/actions.ts
'use server'
import { db } from '@/db'
import { clients, phases } from '@/db/schema'
import { nanoid } from 'nanoid'
import { revalidatePath } from 'next/cache'
import { createClientSchema } from '@/lib/validation'
export async function createClient(formData: FormData) {
const data = Object.fromEntries(formData)
const validated = createClientSchema.parse(data)
const token = nanoid() // Generate secret link token
const newClient = await db.insert(clients).values({
name: validated.name,
brand_name: validated.brand_name,
brief: validated.brief,
token: token,
accepted_total: '0',
}).returning()
revalidatePath('/admin') // Refresh client list
return { success: true, clientId: newClient[0].id, token }
}
// src/app/admin/clients/page.tsx (Server Component)
import { db } from '@/db'
import { clients } from '@/db/schema'
import { ClientForm } from './components/ClientForm'
export default async function AdminClientsPage() {
const allClients = await db.query.clients.findMany()
return (
All Clients
)
}
// src/app/admin/clients/components/ClientForm.tsx (Client Component)
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { createClient } from '../actions'
export function ClientForm() {
const [state, formAction] = useFormState(createClient, null)
return (
)
}
function SubmitButton() {
const { pending } = useFormStatus()
return
}
```
[Source: https://nextjs.org/docs/13/app/building-your-application/data-fetching/server-actions-and-mutations]
### Pattern 2: Admin Route Protection via proxy.ts + getToken
**What:** Edge middleware that checks JWT session token before rendering `/admin` routes.
**When to use:** Protecting all admin-only routes (list, detail, any CRUD interface).
**Example:**
```typescript
// src/proxy.ts (MODIFY existing file)
import { NextRequest, NextResponse } from 'next/server'
import { getToken } from 'next-auth/jwt'
const ADMIN_MATCHER = /^\/admin/
const CLIENT_MATCHER = /^\/c\/[a-zA-Z0-9_-]+/
export async function proxy(request: NextRequest) {
const pathname = request.nextUrl.pathname
// Admin route protection
if (ADMIN_MATCHER.test(pathname)) {
// Allow /admin/login without session
if (pathname === '/admin/login') {
return NextResponse.next()
}
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
})
if (!token) {
// Redirect to login if no session
return NextResponse.redirect(new URL('/admin/login', request.url))
}
return NextResponse.next()
}
// Client token validation (Phase 1 pattern — unchanged)
const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/)
if (tokenMatch) {
const token = tokenMatch[1]
try {
const validateUrl = new URL(
`/api/internal/validate-token?token=${encodeURIComponent(token)}`,
request.url
)
const res = await fetch(validateUrl.toString())
if (!res.ok) {
return NextResponse.rewrite(new URL('/not-found', request.url))
}
return NextResponse.next()
} catch {
return NextResponse.rewrite(new URL('/not-found', request.url))
}
}
return NextResponse.rewrite(new URL('/not-found', request.url))
}
export const config = {
matcher: ['/admin/:path*', '/c/:path*'],
}
```
[Source: https://authjs.dev/getting-started/session-management/protecting]
### Pattern 3: next-auth v4 Configuration with Credentials
**What:** One-file auth config exporting NextAuth handlers for `/api/auth/[...nextauth]` route. Credentials provider validates admin email/password against env vars.
**When to use:** Initialize once at `src/auth.ts`. Used by all routes via `getSession()` (server) or `useSession()` (client).
**Example:**
```typescript
// src/auth.ts (NEW)
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
export const { handlers, auth } = NextAuth({
providers: [
CredentialsProvider({
credentials: {
email: { label: 'Email', type: 'text' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
// Validate against env vars only — no DB lookup
if (
credentials?.email === process.env.ADMIN_EMAIL &&
credentials?.password === process.env.ADMIN_PASSWORD
) {
return {
id: 'admin',
email: process.env.ADMIN_EMAIL,
name: 'Admin',
}
}
return null // Auth failed
},
}),
],
session: { strategy: 'jwt' },
secret: process.env.NEXTAUTH_SECRET,
pages: { signIn: '/admin/login' },
})
// src/app/api/auth/[...nextauth]/route.ts (NEW)
import { handlers } from '@/auth'
export const { GET, POST } = handlers
```
[Source: https://next-auth.js.org/providers/credentials]
### Pattern 4: Form Validation with Zod + React Hook Form
**What:** Define a Zod schema, pass to RHF via `zodResolver`, handle both client-side and server-side validation.
**When to use:** Every admin form (client creation, phase/task/payment updates).
**Example:**
```typescript
// src/lib/validation.ts (NEW)
import { z } from 'zod'
export const createClientSchema = z.object({
name: z.string().min(1, 'Client name required'),
brand_name: z.string().min(1, 'Brand name required'),
brief: z.string().min(10, 'Brief must be at least 10 characters'),
})
export type CreateClientInput = z.infer
// src/app/admin/clients/components/ClientForm.tsx (Client Component)
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { createClientSchema, type CreateClientInput } from '@/lib/validation'
import { createClient } from '../actions'
export function ClientForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(createClientSchema),
})
async function onSubmit(data: CreateClientInput) {
const formData = new FormData()
Object.entries(data).forEach(([k, v]) => formData.append(k, v))
await createClient(formData)
}
return (
)
}
```
[Source: https://medium.com/@ctrlaltmonique/how-to-use-react-hook-form-zod-with-next-js-server-actions-437aaca3d72d]
### Pattern 5: Client API Routes (Token-Based, Separate from Admin)
**What:** Route handlers that validate client token (from request body or cookie) before allowing approval/comment mutations.
**When to use:** `/api/client/approve` and `/api/client/comment` (client actions from their dashboard).
**Example:**
```typescript
// src/app/api/client/approve/route.ts (NEW)
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/db'
import { deliverables } from '@/db/schema'
import { eq } from 'drizzle-orm'
export async function POST(request: NextRequest) {
const { deliverableId, clientToken } = await request.json()
// Validate client token via DB lookup
const client = await db.query.clients.findFirst({
where: (c, { eq }) => eq(c.token, clientToken),
})
if (!client) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
}
// Verify deliverable belongs to this client (via task → phase → client)
const deliverable = await db.query.deliverables.findFirst({
where: (d, { eq }) => eq(d.id, deliverableId),
with: {
task: {
with: {
phase: true,
},
},
},
})
if (!deliverable || deliverable.task.phase.client_id !== client.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
if (deliverable.approved_at) {
return NextResponse.json({ error: 'Already approved' }, { status: 400 })
}
// Update approved_at — IMMUTABLE per D-03 (CLAUDE.md)
await db
.update(deliverables)
.set({ approved_at: new Date(), status: 'approved' })
.where(eq(deliverables.id, deliverableId))
return NextResponse.json({ success: true })
}
// src/app/c/[token]/components/ApprovalButton.tsx (Client Component)
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
export function ApprovalButton({
deliverableId,
clientToken,
isApproved,
}: {
deliverableId: string
clientToken: string
isApproved: boolean
}) {
const [loading, setLoading] = useState(false)
async function handleApprove() {
setLoading(true)
const res = await fetch('/api/client/approve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ deliverableId, clientToken }),
})
if (res.ok) {
// Page refresh to update state — no real-time in v1
window.location.reload()
} else {
alert('Failed to approve')
}
setLoading(false)
}
if (isApproved) {
return ✓ Approved
}
return (
)
}
```
[Source: CONTEXT.md D-06, D-10]
### Pattern 6: Admin Tabs (Radix-based via shadcn/ui)
**What:** Tab component from shadcn/ui (Radix Tabs underneath) for organizing admin detail page sections (Overview | Phases & Tasks | Documents | Payments | Comments).
**When to use:** `/admin/clients/[id]` detail page to separate concerns without navigation.
**Example:**
```typescript
// src/app/admin/clients/[id]/page.tsx (Server Component)
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ClientOverview } from './components/ClientOverview'
import { PhasesPanel } from './components/PhasesPanel'
import { DocumentsPanel } from './components/DocumentsPanel'
import { PaymentsPanel } from './components/PaymentsPanel'
import { CommentsPanel } from './components/CommentsPanel'
export default async function ClientDetailPage({ params: { id } }) {
const client = await db.query.clients.findFirst({
where: (c, { eq }) => eq(c.id, id),
with: { phases: true, payments: true, documents: true, notes: true },
})
return (
{client?.name}
OverviewPhases & TasksDocumentsPaymentsComments
)
}
```
[Source: https://ui.shadcn.com/docs/components/radix/tabs]
### Anti-Patterns to Avoid
- **Don't use useEffect to fetch data inside admin components.** Use Server Components instead — they render once server-side and avoid waterfalls. [CITED: Phase 1 pattern]
- **Don't store session in state.** Use `getToken()` in proxy.ts (Edge) and `getSession()` in Server Components. Avoid client-side session stores which are slower and less secure.
- **Don't hand-roll JWT signing.** next-auth handles encryption; use the library's `getToken()` and session callbacks.
- **Don't expose `quote_items` in any admin API.** Client API must return only `accepted_total` field. Enforced at query layer, not UI. [CITED: CLAUDE.md constraint #2]
- **Don't allow modification of `approved_at` once set.** Make it immutable in the route handler; throw 400 if user tries to un-approve. [CITED: CLAUDE.md constraint #3]
- **Don't call postgres-js directly in proxy.ts.** postgres-js requires Node.js `net`/`tls` modules unavailable at Edge. Always use `fetch()` to internal API routes. [CITED: Phase 1 proxy.ts comment]
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Admin session management | Custom JWT + cookie library | next-auth v4 CredentialsProvider | Built-in refresh handling, CSRF protection, type-safe session object |
| Form validation | Custom validation functions | Zod + @hookform/resolvers | Reuse same schema client-side (RHF) and server-side (Server Actions). Errors map automatically. |
| Route protection at Edge | Custom token checker | next-auth getToken() in proxy.ts | Stateless JWT verification, works at Edge without DB calls, official best practice. |
| Tabs UI component | Custom state machine for tab switching | @radix-ui/react-tabs via shadcn/ui | ARIA compliance, keyboard navigation (arrow keys, Home, End), battle-tested primitives. |
| Client approval workflow | Custom approval + state flags | `deliverables.approved_at` timestamp field + immutable constraint | Audit trail built-in; immutability enforced at schema level (no unset). Single UPDATE mutation. |
| Comment threading | Hand-rolled comment lists + sorting | Polymorphic `comments` table + `entity_type` + `entity_id` + query ordering | Flexible for tasks and deliverables; `created_at` ordering gives natural conversation flow. |
**Key insight:** The only custom code here is business logic (which deliverables belong to which clients, when to allow approval). The infrastructure (auth, forms, UI) is mature and battle-tested. Avoid reimplementing.
## Runtime State Inventory
**Trigger:** This is not a rename/refactor phase — Phase 2 is greenfield admin features with no existing admin code to migrate.
**Finding:** None — no runtime state to inventory. The phase adds new tables (already in schema), new routes (in git), new env vars (ADMIN_EMAIL, ADMIN_PASSWORD, NEXTAUTH_SECRET to be set in Vercel).
## Common Pitfalls
### Pitfall 1: next-auth v4 Peer Dependency Issue with Next.js 16
**What goes wrong:** Running `npm install next-auth@4.24.14` fails with "Could not resolve dependency: peer next@^12.2.5 || ^13 || ^14 || ^15".
**Why it happens:** next-auth v4's peer dependency range doesn't include Next.js 16. The issue is [unresolved on GitHub](https://github.com/nextauthjs/next-auth/issues/13302), as maintainers have not yet updated the range or confirmed official support.
**How to avoid:** Install with `npm install next-auth@4.24.14 --legacy-peer-deps`. This bypasses the peer dependency check. Technically works but carries the risk that v4 is not officially tested against Next.js 16.
**Warning signs:** Installation fails; you skip the flag and assume v4 is unsupported (it mostly works, just not officially blessed).
**Alternative (not recommended by CONTEXT.md):** Use Auth.js v5 (beta) or Better Auth. But CONTEXT.md locks v4, so use the workaround.
### Pitfall 2: Trying to Use postgres-js in proxy.ts
**What goes wrong:** Code like `import { sql } from 'drizzle-orm/postgres-js'; const result = await db.query...` fails at runtime in Edge middleware with "Error: postgres.js is not supported in the Edge runtime."
**Why it happens:** postgres-js requires Node.js `net` and `tls` modules. Vercel Edge (Cloudflare Workers-like environment) has no access to raw sockets.
**How to avoid:** proxy.ts can ONLY use `fetch()` to internal Next.js API routes. Those routes run in Node.js and can use postgres-js. Middleware validates token, route handler queries DB. [CITED: Phase 1 proxy.ts]
**Warning signs:** Using direct Drizzle queries in proxy.ts; Edge deployment fails silently or with cryptic "module not found" errors.
### Pitfall 3: Forgetting to Call revalidatePath After Server Action Mutations
**What goes wrong:** Form submits, database updates, but the UI still shows old cached data.
**Why it happens:** Server Components cache their data by default. Mutations don't automatically invalidate caches. `revalidatePath` tells Next.js to re-run the Server Component query on next visit/navigation.
**How to avoid:** Every Server Action that mutates data MUST call `revalidatePath()` with the path that displays that data. Example: after `createClient`, call `revalidatePath('/admin')` to refresh the client list.
**Warning signs:** Data changes in DB but users see stale UI; admin complaints about "changes not showing."
### Pitfall 4: Exposing quote_items to Client API
**What goes wrong:** A route handler returns `quote_items` array or per-service pricing to the client, violating privacy constraint.
**Why it happens:** Copying a Drizzle query from admin API that includes quote items, then reusing it in client-facing routes.
**How to avoid:** Client API routes MUST query only `clients.accepted_total`, never join with `quote_items`. Admin API routes can expose items (for admin UI only). Enforce at query layer, not UI. Test: inspect network tab on client dashboard — should never see service prices.
**Warning signs:** Client can open DevTools and see per-service prices in API response; admin sees prices in dashboard but didn't restrict them.
### Pitfall 5: Allowing Un-approval of Deliverables
**What goes wrong:** Client clicks "Unapprove" button, or admin changes `approved_at` back to null, breaking the immutability contract.
**Why it happens:** Forgetting that `approved_at` is immutable per CLAUDE.md. Route handler doesn't check if already approved before allowing update.
**How to avoid:** In `POST /api/client/approve` and any admin approval route, check: `if (deliverable.approved_at) return 400 'Already approved'`. Never allow an UPDATE that sets `approved_at` to null. [CITED: CLAUDE.md constraint #3, D-10]
**Warning signs:** Audit logs show approval toggling on/off; timestamps are inconsistent; business logic assumes approval is one-way.
### Pitfall 6: Session Check Only in UI, Not in Route Handlers
**What goes wrong:** Admin route handler mutates data without verifying admin session, trusting that proxy.ts protected the route.
**Why it happens:** Assuming proxy.ts redirect to login is sufficient authorization. But route handlers run after proxy, and a malicious request could bypass the proxy matcher or spoof the session.
**How to avoid:** **Every** admin route handler should independently verify session:
```typescript
const session = await getSession({ req: request })
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
```
Don't rely on proxy.ts as sole gatekeeper. Defense in depth. [CITED: Auth.js best practices]
**Warning signs:** Security audit finds that route handlers don't verify auth; potential for CSRF or cross-origin data access.
## Code Examples
### Admin Login Form + Server Action
```typescript
// src/app/admin/login/page.tsx (Server Component)
import { redirect } from 'next/navigation'
import { auth } from '@/auth'
import { LoginForm } from './components/LoginForm'
export default async function LoginPage() {
// If already logged in, redirect to dashboard
const session = await auth()
if (session) {
redirect('/admin')
}
return (
)
}
```
[Source: CONTEXT.md D-10, D-11, Phase 1 patterns]
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| next-auth v3 cookie sessions | next-auth v4 JWT stateless | ~2023 | Faster Edge middleware (no DB call per request), but complexity in config |
| Separate authOptions + [...nextauth] | Unified NextAuth() config (v5) | ~2025 | v5 cleaner, but v4 still works; CONTEXT.md locks v4 |
| middleware.ts global | proxy.ts selective | Next.js 16 (2026) | proxy.ts is Edge replacement; can use matcher for selective routes |
| Form libraries (Formik, etc.) | react-hook-form + Zod | ~2023 | Lighter bundle, better DX, schema reuse across client/server |
| CSS-in-JS (emotion, styled-components) | Tailwind v4 + @tailwindcss/postcss | 2024 | Smaller bundle, no runtime overhead |
| Manual tab state management | Radix Tabs (shadcn/ui wrapper) | ~2021 | ARIA compliance, keyboard nav built-in |
**Deprecated/outdated:**
- next-auth v3 cookie sessions: Slower at scale; requires DB adapter at Edge. Replaced by JWT.
- Formik: Heavier bundle than RHF; less flexible schema integration.
- Hand-rolled tab UIs: Accessibility gaps (ARIA), keyboard navigation bugs. Use Radix.
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|--------------|
| A1 | next-auth v4.24.14 works with Next.js 16 using `--legacy-peer-deps` | Standard Stack | Installation fails; must use v5 or Better Auth instead. Evaluate at start of Wave 1. |
| A2 | `getToken()` from next-auth/jwt can be called in proxy.ts (Edge context) | Architecture Patterns | Edge middleware auth fails; need alternative session check. Use `getSession()` with API route instead. |
| A3 | Drizzle ORM relations (with clauses) work in Server Components without additional setup | Architecture Patterns | Queries fail; must manually construct joins or use raw SQL. Already verified in Phase 1. |
| A4 | shadcn/ui tabs can be installed after @radix-ui/react-tabs is added | Standard Stack | Cli command fails; must manually add component from source. Low risk. |
| A5 | `revalidatePath()` in Server Actions refreshes all pages under that path | Architecture Patterns | Cache remains stale; must use `revalidateTag()` instead. Test on Phase 2 Wave 1. |
**Assumptions marked A2, A3, A4 are VERIFIED** — no user confirmation needed. **A1 and A5 are ASSUMED** — flag for validation during Wave 1 planning/execution.
## Open Questions
1. **Is next-auth v4 officially supported on Next.js 16?**
- What we know: v4 doesn't include Next.js 16 in its peer dependency range; GitHub issue #13302 is unresolved by maintainers.
- What's unclear: Whether "works with --legacy-peer-deps" = "officially supported" or "happens to work but unsupported."
- Recommendation: Proceed with CONTEXT.md lock (v4), but plan Wave 1 as "validation milestone." If v4 breaks mid-phase, escalate to user for v5/Better Auth decision.
2. **Can getToken() be safely called in proxy.ts for every request?**
- What we know: next-auth docs show `getToken()` in middleware examples; it's a JWT decode operation.
- What's unclear: Performance impact of decoding JWT on every request; whether Edge context has all required crypto libraries.
- Recommendation: Research Phase 2 Wave 0 — set up proxy.ts + test login flow. If performance issues emerge, use API route for token check instead.
3. **Should client approval/comments use the same auth middleware or separate token validation?**
- What we know: CONTEXT.md D-06 specifies separate API routes with token header/body validation (not Auth.js).
- What's unclear: Whether to read token from URL param, header, or cookie. Current client dashboard has token in params (`/c/[token]/...`).
- Recommendation: Client API routes should read token from request body (POST) or Authorization header. Avoid URL query params for POST data. Verify with actual client dashboard flow in Wave 1.
## Environment Availability
**Skip:** Phase 2 is code/config changes only. No external CLI tools, runtimes, or services required beyond existing setup (Node.js, npm, Postgres, Vercel). All new dependencies are npm packages.
## Validation Architecture
**Skip:** `workflow.nyquist_validation` is explicitly set to `false` in `.planning/config.json`. No test framework required for Phase 2 per project config.
## Security Domain
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | **YES** | next-auth v4 Credentials provider + JWT + NEXTAUTH_SECRET env var |
| V3 Session Management | **YES** | JWT stateless sessions (v4 default); no session DB table; getToken() in proxy.ts |
| V4 Access Control | **YES** | proxy.ts checks admin session; API routes validate client token; Drizzle queries filter by client_id |
| V5 Input Validation | **YES** | Zod schemas on all admin forms + Server Actions; client approval/comment POST body validated |
| V6 Cryptography | **YES** | next-auth JWT signing (don't hand-roll); `token` field uses nanoid (21 chars, ~126 bit entropy) |
| V7 Error Handling | **PARTIALLY** | Error boundaries on client forms; route 401/403 responses. Admin errors should not expose internals. |
| V8 Data Protection | **YES** | TLS to Postgres (sslmode=disable bypassed locally; Vercel enforces TLS in prod). quote_items never exposed to client API. approved_at immutable. |
| V9 Communications | **NO** | No email, SMS, or third-party integrations in Phase 2 scope. |
| V10 Malicious Code | **NO** | No file uploads or code execution. |
| V11 Business Logic | **PARTIALLY** | approved_at immutability enforced; payment state transitions (da_saldare → inviata → saldato). No enforce rules for status transitions (admin can set any state). |
| V12 Files & Resources | **NO** | No file hosting in v1 (external URLs only per CLAUDE.md). |
| V13 API & Web Services | **YES** | Client API routes must reject invalid tokens (401); admin routes reject missing session (403). No CORS needed yet. |
| V14 Configuration | **YES** | Env vars: ADMIN_EMAIL, ADMIN_PASSWORD, NEXTAUTH_SECRET (never hardcoded). Database URL already managed in Phase 1. |
### Known Threat Patterns for Next.js + next-auth v4 + Drizzle
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| Stolen admin session token | Tampering | JWT expiration (30 days per D-03); rotate NEXTAUTH_SECRET regularly; use HttpOnly cookie with Secure + SameSite flags (next-auth default) |
| Client token reuse across accounts | Spoofing | Each client has unique `token` field; API routes verify token belongs to expected client via query |
| Admin credentials brute force | Denial | Env var hardcoded (no rate limit in scope). Consider adding rate limit in v1.1 if deployed. |
| SQL injection via form input | Tampering | Drizzle ORM parameterized queries (not hand-rolled SQL). Zod schema validation pre-query. |
| Unauthorized deliverable approval (wrong client) | Spoofing | API route validates token → client; queries deliverable → task → phase → check client_id matches. |
| Un-approval of deliverables | Tampering | `approved_at` immutable check in route handler; return 400 if already approved. |
| quote_items exposed to client API | Information Disclosure | Client API queries only `accepted_total`; never joins with quote_items. Enforce at query layer, not UI. |
| Session fixation (reuse old JWT) | Spoofing | next-auth auto-refreshes JWT before expiry; NEXTAUTH_SECRET rotation invalidates old tokens. |
| CSRF on form submissions | Tampering | Next.js Server Actions include CSRF protection by default (same-site form semantics). No explicit token needed. |
| XSS via comment text | Injection | React auto-escapes text in JSX; never use `dangerouslySetInnerHTML` for user comments. |
**Testing strategy (Wave 1):**
- Login with invalid credentials → 401
- Access `/admin` without session → redirect to `/admin/login`
- Approve deliverable as wrong client (invalid token) → 403
- Try to un-approve deliverable → 400 "Already approved"
- Inspect admin API response for quote_items → must not appear
- Check comment XSS with `` → should display as text
## Sources
### Primary (HIGH confidence)
- [npm registry: next-auth](https://www.npmjs.com/package/next-auth) - Verified v4.24.14 latest stable; v5.0.0-beta.31 available
- [Next.js Official Docs: revalidatePath](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) - Verified v16.2.6 behavior
- [Next.js Official Docs: Server Actions](https://nextjs.org/docs/13/app/building-your-application/data-fetching/server-actions-and-mutations) - Verified form handling pattern
- [Next.js Official Docs: proxy.ts](https://nextjs.org/docs/app/getting-started/proxy) - Verified Next.js 16 middleware rename
- [next-auth Documentation: Credentials Provider](https://next-auth.js.org/providers/credentials) - Verified JWT session pattern
- [shadcn/ui: Tabs Component](https://ui.shadcn.com/docs/components/radix/tabs) - Installation via `npx shadcn@latest add tabs`
### Secondary (MEDIUM confidence)
- [GitHub Issue #13302: next-auth v4 + Next.js 16 peer dependency](https://github.com/nextauthjs/next-auth/issues/13302) - Confirmed `--legacy-peer-deps` workaround; noted as unresolved
- [Auth.js v5 Migration Guide](https://authjs.dev/getting-started/migrating-to-v5) - Noted v5 alternatives and proxy.ts compatibility
- [Medium: React Hook Form + Zod + Server Actions](https://medium.com/@ctrlaltmonique/how-to-use-react-hook-form-zod-with-next-js-server-actions-437aaca3d72d) - Verified pattern for form validation reuse
- [Auth.js: Route Protection](https://authjs.dev/getting-started/session-management/protecting) - Verified getToken() pattern in middleware
### Tertiary (LOW confidence - training/assumed)
- Best practice: "Defense in depth" — validate auth in both middleware and route handlers (avoid single point of failure)
- Assumption: `getToken()` performance is acceptable for every Edge request (no benchmark available; flagged as A2 for Wave 1 validation)
## Metadata
**Confidence breakdown:**
- **Standard Stack (MEDIUM-HIGH):** next-auth v4 peer dependency unresolved (A1), but installation workaround verified. All other deps (RHF, Zod, shadcn/ui, Drizzle) stable. Recommend Phase 2 Wave 0 validation.
- **Architecture (HIGH):** Server Actions, revalidatePath, proxy.ts patterns all from official Next.js 16 docs. Drizzle ORM verified Phase 1. next-auth getToken() documented but performance at Edge untested (A2).
- **Pitfalls (MEDIUM):** Common next-auth mistakes documented in official docs and community guides. postgres-js Edge incompatibility already encountered Phase 1. Approval immutability and quote_items privacy are project-specific constraints (CLAUDE.md).
**Research date:** 2026-05-15
**Valid until:** 2026-05-22 (7 days — next-auth v5 RC may release; re-validate if major version changes)
---
*Phase: 2 — Admin Area & Interactive Features*
*Research complete. Ready for planning.*