Files
2026-03-19 16:04:17 -06:00

28 KiB
Raw Permalink Blame History

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 /portal prefix (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 45. 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 2030 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

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.id crashes. Always const { id } = await params first.
  • 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 contentEditable or inline input trick.
  • Separate page for "create client": The decision is modal-on-client-list — do not create /portal/clients/new/page.tsx.
  • Calling revalidatePath without '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, never onMouseEnter/onMouseLeave on 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 using useActionState from react
  • Synchronous params access: Works in dev (compat shim) but deprecated and will fail at build
  • tailwind.config.js content 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

  1. middleware.ts: Change matcher to ["/agent/:path*", "/portal/:path*"]
  2. auth.config.ts authorized callback: Add nextUrl.pathname.startsWith("/portal") to protected routes check; change post-login redirect from /agent/dashboard to /portal/dashboard
  3. /agent/(protected)/dashboard/page.tsx: Replace with redirect to /portal/dashboard (or delete and let the middleware redirect handle it)
  4. 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

  1. Should /agent/(protected)/ be removed or left as redirect shell?

    • What we know: The existing layout.tsx and dashboard/page.tsx under /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.tsx with redirect("/portal/dashboard") and update auth.config.ts post-login redirect. Keep /agent/(protected)/layout.tsx as a minimal pass-through or delete it — the portal layout replaces it.
  2. 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. The searchParams prop on the dashboard page handles this without any client JS.
  3. lastActivityDate on client cards

    • What we know: CONTEXT.md says client cards show "last activity date"
    • What's unclear: Whether this is clients.updatedAt or the MAX(documents.sentAt) for that client
    • Recommendation: Use MAX(documents.sentAt) via a Drizzle subquery, falling back to clients.createdAt if 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 12
  • 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)