28 KiB
Phase 3: Agent Portal Shell - Research
Researched: 2026-03-19 Domain: Next.js 16 App Router — authenticated portal UI, Drizzle ORM schema extension, Server Actions, Tailwind v4 utility-class modals Confidence: HIGH
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
Portal Navigation
- Top nav (horizontal nav bar) — not sidebar
- Agent lands on Dashboard after login
- All portal routes live under a
/portalprefix (e.g.,/portal/dashboard,/portal/clients,/portal/clients/[id]) - Visual feel: same brand as marketing site (shared colors/fonts) but cleaner and more utilitarian — clearly a logged-in app, not a marketing page
Client List Design
- Card grid layout (not table/list)
- Each card shows: client name, email, document count, last activity date
- New client created via modal/dialog triggered from the client list page (not a separate page)
- Empty state: friendly message ("No clients yet") with a prominent "+ Add your first client" CTA button
Dashboard Document View
- Table layout with color-coded status badges: Draft=gray, Sent=blue, Viewed=amber/yellow, Signed=green
- Columns: Document name, Client, Status badge, Date sent
- Controls: filter by status (dropdown) + sort by date (most recent first)
- Phase 3 seeds a few placeholder document rows (real documents arrive in Phase 4)
- Empty state not needed since data is seeded
Client Profile Page
- Two-section layout: client details header (name, email, edit button) above a documents table
- Back link to Clients list
- Edit client: modal/dialog (consistent with create pattern) — no inline editing
- Documents section: same table style as dashboard (doc name, status badge, date)
- Delete client: delete button on profile with confirmation dialog before removing
Claude's Discretion
- Exact card dimensions and spacing on client grid
- Color palette values for status badges (within brand colors)
- Skeleton/loading states
- Exact wording for empty states and confirmation dialogs
- Mobile responsiveness details
Deferred Ideas (OUT OF SCOPE)
- None — discussion stayed within phase scope </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| CLIENT-01 | Agent can create a client record with name and email address | Server Action + Drizzle insert pattern; modal form with useActionState |
| CLIENT-02 | Agent can view a list of all clients | Drizzle select with LEFT JOIN to count documents; card grid layout |
| CLIENT-03 | Agent can view a client's profile and their associated documents | Dynamic route /portal/clients/[id]; params is a Promise in Next.js 16; two-section layout |
| DASH-01 | Agent can see all documents with their current status: Draft / Sent / Viewed / Signed | Placeholder documents table seeded in DB; status badge pattern with Tailwind |
| DASH-02 | Agent can see which client each document was sent to and when | Drizzle JOIN across clients + documents tables; columns: document name, client, status, date sent |
| </phase_requirements> |
Summary
This phase builds the authenticated portal shell for the Teressa Copeland Homes agent. The existing foundation (Phase 1) established authentication under /agent/:path* with Next-Auth v5 beta. Phase 3 extends to a new /portal prefix with a proper top-nav layout and three views: Dashboard, Clients list, and Client Profile.
The most important integration challenge is the routing prefix change: CONTEXT.md requires /portal/dashboard, /portal/clients, etc., but the existing middleware and auth config protect /agent/:path* and redirect logins to /agent/login. The middleware matcher and auth.config.ts authorized callback must be updated to also cover /portal/:path*. The existing /agent/(protected)/ route group should be migrated or the portal routes added as a new protected segment. The login redirect should remain /agent/login (same page, different portal prefix).
The schema needs two new tables: clients and documents. The documents table in Phase 3 is a stub — it holds name, status, client_id, and sent_at. Real document content (PDF blobs, signature fields) arrives in Phases 4–5. Phase 3 seeds placeholder document rows using the existing scripts/seed.ts pattern via npm run db:seed.
Primary recommendation: Add /portal to middleware matcher alongside /agent; create src/app/portal/(protected)/ route group with its own layout.tsx containing the top-nav; add clients and documents (stub) tables to schema; all mutations via Server Actions with revalidatePath; modals as pure client components with useActionState.
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| Next.js | 16.2.0 (pinned in package.json) | App Router, Server Actions, layouts, dynamic routes | Already in project; provides SSR + Server Actions for free |
| Drizzle ORM | ^0.45.1 (in package.json) | Schema definition, migrations, typed queries | Already established pattern in project |
| next-auth | 5.0.0-beta.30 (pinned) | Session access in Server Components, middleware | Pinned exact version — DO NOT upgrade |
| Zod | ^4.3.6 (in package.json) | Server Action input validation | Already used in auth.ts; same pattern for client forms |
| Tailwind CSS | ^4 (in package.json) | Styling — utility classes, CSS variables via @theme |
Already configured; brand variables defined in globals.css |
| lucide-react | ^0.577.0 (in package.json) | Icons (UserPlus, Users, LayoutDashboard, etc.) | Already installed in Phase 2 |
| postgres | ^3.4.8 (in package.json) | PostgreSQL driver | Already used |
Supporting
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| tsx | ^4.21.0 (devDep) | Run seed scripts | Already used for scripts/seed.ts |
| drizzle-kit | ^0.31.10 (devDep) | Generate and run migrations | Already used |
Alternatives Considered
| Instead of | Could Use | Tradeoff |
|---|---|---|
| Plain Tailwind modals | shadcn/ui Dialog | shadcn is not installed; adding it adds complexity; raw Tailwind modals are 20–30 lines and fully controlled |
| Server Action mutations | Route Handlers (API routes) | Server Actions are simpler — no fetch, no JSON, same validation pipeline; already used in Phase 2 (contact form) |
| Separate "create" page | Route-intercepting modal | Intercepting routes add parallel route complexity; client-side state modal is simpler for a single-agent app |
Installation: No new packages needed. All dependencies are already installed.
Architecture Patterns
Recommended Project Structure
src/app/
├── agent/ # Existing — keep intact
│ ├── login/ # Existing login page
│ └── (protected)/ # Existing minimal layout — CAN BE MIGRATED
│ └── dashboard/ # Phase 1 stub — REPLACE with redirect to /portal/dashboard
├── portal/ # NEW — Phase 3
│ ├── (protected)/ # Route group — applies portal layout
│ │ ├── layout.tsx # Top-nav + auth check
│ │ ├── dashboard/
│ │ │ └── page.tsx # DASH-01, DASH-02
│ │ ├── clients/
│ │ │ └── page.tsx # CLIENT-01, CLIENT-02 (card grid + create modal)
│ │ └── clients/
│ │ └── [id]/
│ │ └── page.tsx # CLIENT-03 (profile + documents table)
│ └── _components/ # Portal-scoped components (not shared with marketing)
│ ├── PortalNav.tsx # Top nav component
│ ├── ClientCard.tsx # Card for clients grid
│ ├── ClientModal.tsx # Create/edit modal (shared mode prop)
│ ├── ConfirmDialog.tsx # Delete confirmation dialog
│ ├── StatusBadge.tsx # Color-coded status badge
│ └── DocumentsTable.tsx # Reusable table for dashboard + profile
src/lib/
├── db/
│ ├── schema.ts # ADD: clients + documents tables
│ └── index.ts # Existing lazy proxy singleton — unchanged
└── actions/
└── clients.ts # NEW: createClient, updateClient, deleteClient server actions
scripts/
└── seed.ts # EXTEND: add client + placeholder document rows
drizzle/
└── 0001_....sql # NEW migration for clients + documents tables
Pattern 1: Portal Layout with Top Nav and Auth Check
What: A route-group layout that checks session and renders top nav. When to use: Every portal page.
// src/app/portal/(protected)/layout.tsx
// Source: /teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/layout.md
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { PortalNav } from "../_components/PortalNav";
export default async function PortalLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session) redirect("/agent/login");
return (
<div className="min-h-screen bg-[var(--cream)]">
<PortalNav userEmail={session.user?.email ?? ""} />
<main className="max-w-7xl mx-auto px-6 py-8">{children}</main>
</div>
);
}
Pattern 2: Dynamic Route — params is a Promise (Next.js 16)
What: In Next.js 16, params prop is a Promise, not a plain object.
When to use: Every page with [id] segment.
// src/app/portal/(protected)/clients/[id]/page.tsx
// Source: /teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/page.md
export default async function ClientProfilePage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// db query with id...
}
Pattern 3: Server Action with Zod Validation + revalidatePath
What: Validated server mutation that refreshes the relevant page cache. When to use: createClient, updateClient, deleteClient.
// src/lib/actions/clients.ts
// Source: /teressa-copeland-homes/node_modules/next/dist/docs/01-app/02-guides/forms.md
"use server";
import { z } from "zod";
import { db } from "@/lib/db";
import { clients } from "@/lib/db/schema";
import { auth } from "@/lib/auth";
import { revalidatePath } from "next/cache";
const clientSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Valid email required"),
});
export async function createClient(
_prevState: { error?: string } | null,
formData: FormData
) {
const session = await auth();
if (!session) return { error: "Unauthorized" };
const parsed = clientSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors.name?.[0] ?? "Invalid input" };
}
await db.insert(clients).values(parsed.data);
revalidatePath("/portal/clients");
return { success: true };
}
Pattern 4: Client Component Modal with useActionState
What: Modal opened by client-side state, form submits Server Action, closes on success. When to use: Create client, edit client dialogs.
// src/app/portal/_components/ClientModal.tsx
// Source: /teressa-copeland-homes/node_modules/next/dist/docs/01-app/02-guides/forms.md (useActionState)
"use client";
import { useActionState, useEffect } from "react";
import { createClient } from "@/lib/actions/clients";
type Props = {
isOpen: boolean;
onClose: () => void;
};
export function ClientModal({ isOpen, onClose }: Props) {
const [state, formAction, pending] = useActionState(createClient, null);
useEffect(() => {
if (state?.success) onClose();
}, [state, onClose]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-xl shadow-xl p-6 w-full max-w-md">
<h2 className="text-lg font-semibold text-[var(--navy)] mb-4">Add Client</h2>
<form action={formAction} className="space-y-4">
<input name="name" type="text" placeholder="Full name" required
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm" />
<input name="email" type="email" placeholder="Email address" required
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm" />
{state?.error && (
<p className="text-sm text-red-600">{state.error}</p>
)}
<div className="flex justify-end gap-3 pt-2">
<button type="button" onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-900">
Cancel
</button>
<button type="submit" disabled={pending}
className="px-4 py-2 text-sm bg-[var(--navy)] text-white rounded-lg disabled:opacity-60">
{pending ? "Saving..." : "Add Client"}
</button>
</div>
</form>
</div>
</div>
);
}
Pattern 5: Status Badge Component
What: Reusable color-coded pill for document status. When to use: Dashboard table + client profile documents table.
// src/app/portal/_components/StatusBadge.tsx
type DocumentStatus = "Draft" | "Sent" | "Viewed" | "Signed";
const STATUS_STYLES: Record<DocumentStatus, string> = {
Draft: "bg-gray-100 text-gray-600",
Sent: "bg-blue-100 text-blue-700",
Viewed: "bg-amber-100 text-amber-700",
Signed: "bg-green-100 text-green-700",
};
export function StatusBadge({ status }: { status: DocumentStatus }) {
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${STATUS_STYLES[status]}`}>
{status}
</span>
);
}
Pattern 6: Drizzle Schema with Relations
What: New clients and documents (stub) tables with foreign key.
When to use: Phase 3 schema migration.
// src/lib/db/schema.ts — additions
// Source: Drizzle ORM docs (verified against node_modules/drizzle-orm)
import { pgTable, text, timestamp, pgEnum } from "drizzle-orm/pg-core";
export const users = pgTable("users", { /* existing */ });
export const clients = pgTable("clients", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
email: text("email").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const documentStatusEnum = pgEnum("document_status", [
"Draft", "Sent", "Viewed", "Signed"
]);
export const documents = pgTable("documents", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
clientId: text("client_id").notNull().references(() => clients.id, { onDelete: "cascade" }),
status: documentStatusEnum("status").notNull().default("Draft"),
sentAt: timestamp("sent_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
Pattern 7: Dashboard Query with JOIN
What: Fetch all documents with client name in one query. When to use: Dashboard page server component data fetch.
// Inside dashboard/page.tsx (server component)
import { db } from "@/lib/db";
import { documents, clients } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
const rows = await db
.select({
id: documents.id,
name: documents.name,
status: documents.status,
sentAt: documents.sentAt,
clientName: clients.name,
clientId: documents.clientId,
})
.from(documents)
.leftJoin(clients, eq(documents.clientId, clients.id))
.orderBy(/* desc(documents.sentAt) or desc(documents.createdAt) */);
Anti-Patterns to Avoid
- Synchronous params access on Next.js 16:
params.idcrashes. Alwaysconst { id } = await paramsfirst. - Fetching data in Client Components with useEffect: Portal pages are server components — query DB directly, pass data as props to client child components.
- Inline editing of client fields: CONTEXT.md locked this to modal/dialog only — no
contentEditableor inline input trick. - Separate page for "create client": The decision is modal-on-client-list — do not create
/portal/clients/new/page.tsx. - Calling
revalidatePathwithout 'use server' boundary: It only works in Server Functions and Route Handlers, not in Client Components. - Hover event handlers on Server Components: Already a known project pitfall from Phase 2 — use Tailwind
hover:classes, neveronMouseEnter/onMouseLeaveon server components.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| Form validation | Custom validators | Zod (already installed) | Already established pattern in auth.ts |
| Dialog/modal | CSS + JS from scratch | 30-line Tailwind modal pattern (see Pattern 4 above) | Simpler than installing shadcn for two dialogs |
| UUID generation | uuid package |
crypto.randomUUID() (already used in schema.ts) |
Built-in, already established |
| Cache invalidation after mutation | Router.refresh() hack | revalidatePath from next/cache |
Official Next.js 16 pattern for server actions |
| Filter/sort state | URL query string manually | useSearchParams + useRouter |
Filter dropdown state can be URL-driven for shareability |
Key insight: The project already has all necessary packages. Adding shadcn, headlessui, or any other UI component library is unnecessary overhead for the two modals this phase requires.
Common Pitfalls
Pitfall 1: Middleware Doesn't Cover /portal
What goes wrong: The existing middleware.ts matcher is ["/agent/:path*"] only. Routes under /portal/ will not be protected — unauthenticated users can access portal pages.
Why it happens: Middleware matcher is explicitly set to /agent only.
How to avoid: Update middleware.ts matcher to ["/agent/:path*", "/portal/:path*"] AND update auth.config.ts authorized callback to include nextUrl.pathname.startsWith("/portal") check.
Warning signs: Dashboard page renders without session check passing.
Pitfall 2: Login Redirect Loops with New /portal Prefix
What goes wrong: The authorized callback in auth.config.ts redirects to /agent/dashboard on successful login from login page. If the agent is already on a /portal route, double-redirect can occur.
Why it happens: Hard-coded redirect destination in authorized callback.
How to avoid: Change the login-success redirect in auth.config.ts from /agent/dashboard to /portal/dashboard. Keep the login page at /agent/login (user decision: portal prefix is /portal/dashboard, not /portal/login).
Warning signs: After login, browser navigates to /agent/dashboard (old route) not /portal/dashboard.
Pitfall 3: params Is a Promise in Next.js 16
What goes wrong: const { id } = params on a client profile page returns undefined; the page renders with no data or throws.
Why it happens: Next.js 15+ changed params from a synchronous object to a Promise. In Next.js 16, accessing it synchronously may still "work" during local dev (backwards compat shim) but causes issues at build time.
How to avoid: Always const { id } = await params in async server components.
Warning signs: id is undefined on profile page, or TypeScript error about Promise<{id:string}> not having .id.
Pitfall 4: revalidatePath Import Location
What goes wrong: import { revalidatePath } from "next/cache" fails or throws in a Client Component.
Why it happens: revalidatePath is server-only. It must be in a 'use server' file.
How to avoid: Keep all revalidatePath calls inside src/lib/actions/*.ts files marked 'use server'.
Warning signs: Runtime error "revalidatePath is not a function" or "Cannot import server-only module."
Pitfall 5: Drizzle pgEnum Must Be Exported and Used in Migration
What goes wrong: pgEnum values in schema don't appear in the migration SQL, causing runtime Drizzle errors when inserting.
Why it happens: If the enum is defined but not exported or referenced by a table column, drizzle-kit generate may skip it.
How to avoid: Export the enum, reference it in the table column, then run npm run db:generate && npm run db:migrate.
Warning signs: invalid input value for enum PostgreSQL error when seeding.
Pitfall 6: useActionState Imported from react Not react-dom
What goes wrong: useFormState / wrong import path causes runtime error.
Why it happens: This was a known project pitfall in Phase 2 — useActionState is from 'react' in React 19, not 'react-dom'.
How to avoid: import { useActionState } from 'react' — the project already has this pattern in the contact form.
Warning signs: "useActionState is not a function" or "useFormState is not exported."
Pitfall 7: Brand Colors in Tailwind v4 — Use CSS Variables
What goes wrong: One-off hex values like bg-[#1B2B4B] get missed by Tailwind v4 JIT and don't appear in output.
Why it happens: Phase 2 pitfall documented in STATE.md: "Brand colors applied via inline style props — Tailwind JIT may miss one-off hex values."
How to avoid: Use CSS variable references: bg-[var(--navy)], text-[var(--gold)]. The variables are defined in globals.css.
Warning signs: Background color doesn't appear in production build; works in dev but not build.
Code Examples
Verified patterns from official sources:
Drizzle Insert with onConflictDoNothing (seed pattern)
// Source: scripts/seed.ts (existing project pattern)
await db.insert(clients).values([
{ name: "Sarah Johnson", email: "sarah.j@example.com" },
{ name: "Mike Torres", email: "m.torres@example.com" },
]).onConflictDoNothing();
Drizzle Delete with WHERE
// Source: drizzle-orm (installed in project, verified in node_modules)
import { eq } from "drizzle-orm";
await db.delete(clients).where(eq(clients.id, id));
revalidatePath After Mutation
// Source: /teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/04-functions/revalidatePath.md
import { revalidatePath } from "next/cache";
revalidatePath("/portal/clients");
revalidatePath(`/portal/clients/${id}`);
Filter by Status (URL search params for filter state)
// src/app/portal/(protected)/dashboard/page.tsx
export default async function DashboardPage({
searchParams,
}: {
searchParams: Promise<{ status?: string; sort?: string }>;
}) {
const { status, sort } = await searchParams;
// filter docs in query based on status param
}
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
Sync params access |
params is a Promise → must await |
Next.js 15+ | All dynamic pages MUST be async and await params |
useFormState from react-dom |
useActionState from react |
React 19 | Import from react, not react-dom |
tailwind.config.js with content paths |
@import "tailwindcss" in CSS, no config file |
Tailwind v4 | No tailwind.config.ts needed; CSS variables via @theme inline |
| Server Actions imported from page files | Dedicated actions/ files with 'use server' at top |
Next.js 15+ convention | Cleaner separation; action files can be shared across multiple pages |
Deprecated/outdated:
useFormState(react-dom): Removed in React 19 — project already usinguseActionStatefromreact- Synchronous
paramsaccess: Works in dev (compat shim) but deprecated and will fail at build tailwind.config.jscontent scanning: Replaced by@import "tailwindcss"in Tailwind v4 — already done in this project
Routing Architecture Notes
Current state (Phase 1/2)
/agent/login — login page
/agent/(protected)/ — route group, layout.tsx with basic auth check + logout
/agent/(protected)/dashboard — stub "coming in Phase 3" page
middleware.ts matcher: ["/agent/:path*"]
auth.config.ts: redirect logged-in users on login page → /agent/dashboard
Required changes for Phase 3
- middleware.ts: Change matcher to
["/agent/:path*", "/portal/:path*"] - auth.config.ts authorized callback: Add
nextUrl.pathname.startsWith("/portal")to protected routes check; change post-login redirect from/agent/dashboardto/portal/dashboard /agent/(protected)/dashboard/page.tsx: Replace with redirect to/portal/dashboard(or delete and let the middleware redirect handle it)- New
src/app/portal/(protected)/layout.tsx: Full portal nav layout
This migration is clean — no destructive changes to auth. Login page stays at /agent/login.
Open Questions
-
Should
/agent/(protected)/be removed or left as redirect shell?- What we know: The existing
layout.tsxanddashboard/page.tsxunder/agent/(protected)/are Phase 1 stubs meant to be replaced here - What's unclear: Whether any external link or test points to
/agent/dashboard - Recommendation: Replace
/agent/(protected)/dashboard/page.tsxwithredirect("/portal/dashboard")and updateauth.config.tspost-login redirect. Keep/agent/(protected)/layout.tsxas a minimal pass-through or delete it — the portal layout replaces it.
- What we know: The existing
-
Dashboard filter state: URL params vs. client state
- What we know: CONTEXT.md specifies filter dropdown and sort controls on dashboard
- What's unclear: Whether filter should survive page refresh (URL params) or be ephemeral (client state)
- Recommendation: URL search params (
?status=Signed) — enables bookmarking and is the Next.js idiomatic approach for filter state in server components. ThesearchParamsprop on the dashboard page handles this without any client JS.
-
lastActivityDateon client cards- What we know: CONTEXT.md says client cards show "last activity date"
- What's unclear: Whether this is
clients.updatedAtor theMAX(documents.sentAt)for that client - Recommendation: Use
MAX(documents.sentAt)via a Drizzle subquery, falling back toclients.createdAtif no documents. This is more meaningful than update timestamp.
Sources
Primary (HIGH confidence)
/teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/page.md— params as Promise, page conventions/teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/layout.md— layout conventions, params/teressa-copeland-homes/node_modules/next/dist/docs/01-app/02-guides/forms.md— Server Actions, useActionState, form validation with Zod/teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/04-functions/revalidatePath.md— cache invalidation after mutations/teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/intercepting-routes.md— modal routing options (considered, not used)/teressa-copeland-homes/src/lib/db/schema.ts— existing schema pattern/teressa-copeland-homes/src/lib/auth.config.ts— existing middleware/auth config/teressa-copeland-homes/middleware.ts— existing matcher config/teressa-copeland-homes/src/app/globals.css— brand CSS variables/teressa-copeland-homes/package.json— exact installed versions
Secondary (MEDIUM confidence)
- drizzle-orm installed in
node_modules/drizzle-orm— pgEnum, relations, query patterns verified by inspecting existing schema.ts usage and drizzle docs folder (not exhaustively read, but schema pattern consistent with existing code)
Tertiary (LOW confidence)
- None
Metadata
Confidence breakdown:
- Standard stack: HIGH — all packages already in project, verified in package.json
- Architecture: HIGH — routing docs read directly from installed Next.js 16 dist; existing project structure inspected
- Pitfalls: HIGH — derived from official docs + STATE.md accumulated decisions from Phases 1–2
- Schema patterns: HIGH — matches existing schema.ts conventions exactly
Research date: 2026-03-19 Valid until: 2026-04-19 (next-auth beta and Next.js 16 move fast; recheck if upgrading either)