Files
red/.planning/phases/01-foundation/01-RESEARCH.md

534 lines
27 KiB
Markdown

# Phase 1: Foundation - Research
**Researched:** 2026-03-19
**Domain:** Next.js 15 App Router, Auth.js v5 credentials auth, Drizzle ORM + Neon PostgreSQL, Vercel Blob, Vercel deployment
**Confidence:** HIGH
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **Login page:** Branded design with Teressa's logo, brand colors, and photo. Route: `/agent/login`. Title: "Agent Portal". Generic error on failure: "Invalid email or password". No forgot-password flow. Password visibility toggle included. Post-login redirect: `/agent/dashboard`.
- **Session behavior:** 7-day rolling session, refreshes on each visit. Persistent across browser restarts (httpOnly cookie, not sessionStorage). On session expiry: silent redirect to `/agent/login`. Logout: immediate redirect to `/agent/login` with "You've been signed out" confirmation message.
- **Database schema:** Auth tables only (users + sessions). ORM: Drizzle ORM. Password storage: bcrypt hashing. Migration files committed to `/drizzle` directory. Initial account: seed script from environment variables (no signup UI). Single-agent design — standard users table rows, no multi-tenant columns.
- **Deployment:** Production only at teressacopelandhomes.com. Secrets via Vercel dashboard only (never committed). Vercel Blob: single store, organized by path (`/documents/signed/`, `/documents/templates/`). Deployment trigger: Vercel native Git integration (push to main → auto-deploy).
### Claude's Discretion
- Exact brand color palette and visual design details (Teressa's brand assets to be used, layout specifics are open)
- Loading/submitting state on the login form button
- Exact bcrypt salt rounds
- Session token storage implementation details (Auth.js/NextAuth vs custom JWT)
### Deferred Ideas (OUT OF SCOPE)
- Forgot password / email reset flow — backlog for a future phase
- Multi-agent support / role-based access — intentionally deferred, single-agent product for now
- Staging or preview environments — production-only for this build
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| AUTH-01 | Agent (Teressa) can log in to the portal with email and password | Auth.js v5 Credentials provider with bcryptjs password comparison; `authorize()` callback in `auth.ts` |
| AUTH-02 | Agent session persists across browser refresh and tab closes | Auth.js v5 JWT strategy with `maxAge: 7 * 24 * 60 * 60`; httpOnly encrypted cookie is the default; no sessionStorage |
| AUTH-03 | All agent portal routes are protected — unauthenticated users are redirected to login | `middleware.ts` with `auth` export and `matcher: ["/agent/:path*"]`; `authorized` callback redirects to `/agent/login` |
| AUTH-04 | Agent can log out from any portal page | Auth.js `signOut()` server action with `redirectTo: "/agent/login"` and a `?signed_out=1` query param or a dedicated confirmation route |
</phase_requirements>
---
## Summary
Phase 1 establishes the entire infrastructure foundation: a Next.js 15 App Router project, Drizzle ORM schema against Neon PostgreSQL, Vercel Blob bucket provisioning, and single-agent authentication via Auth.js v5. Every subsequent phase builds on top of this layer without touching these concerns again.
The authentication stack is well-defined by the user decisions: Auth.js v5 (beta) with the Credentials provider handles email/password login, JWT session strategy stores an encrypted httpOnly cookie, and Next.js middleware protects all `/agent/*` routes. The Drizzle adapter for Auth.js exists but is only needed if using database sessions — since the decision defers to Claude's discretion, the JWT strategy (no sessions table required, simpler schema) is recommended and matches the rolling-session requirement without database writes per request.
The Neon + Drizzle + Vercel deployment triplet is a first-class supported combination with official documentation, making this phase a well-trodden path in 2026. The main risk areas are: Auth.js v5 is still in beta (use `next-auth@beta`, pin a specific beta version), the `signOut` server action has known quirks in Next.js 15 that must be handled correctly, and the Vercel Blob bucket must be created and linked to the project before environment variables are available.
**Primary recommendation:** Use Auth.js v5 (beta) with JWT strategy + Credentials provider. Skip the Drizzle adapter (no sessions table needed). Keep the schema minimal: one `users` table with `id`, `email`, `password_hash`, `created_at`. Run one migration, seed Teressa's account, deploy to Vercel, wire env vars.
---
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| next | 15.x (latest) | Full-stack React framework | Locked by client; App Router is the current default |
| next-auth | 5.0.0-beta (pin latest beta) | Authentication — credentials, session, middleware | Official Next.js auth solution; only option with full App Router support |
| drizzle-orm | ^0.40+ (latest stable) | TypeScript-native ORM for Neon | TypeScript-first, excellent Next.js + Neon support, locked by user |
| drizzle-kit | ^0.30+ (latest stable) | Migration generation and running | Required companion to drizzle-orm; locked by user |
| @neondatabase/serverless | ^0.10+ | Neon HTTP driver for serverless | Required for Vercel serverless functions — uses HTTP not persistent TCP |
| bcryptjs | ^2.4.3 | Password hashing | Pure JS (no native binaries), works in all serverless environments |
| @vercel/blob | ^0.27+ | File/blob storage | Locked by user decision; single store for all documents |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| @types/bcryptjs | ^2.4.x | TypeScript types for bcryptjs | Development dependency — required for TypeScript projects |
| tsx | ^4.x | Run TypeScript scripts directly | Used to run the seed script (`drizzle:seed`) |
| zod | ^3.x | Runtime input validation | Validate login form credentials in `authorize()` callback |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Auth.js v5 JWT strategy | Database session strategy | Database strategy requires a `sessions` table and a write on every visit; JWT is simpler for single-user and satisfies the rolling-session requirement with `updateAge` |
| bcryptjs | bcrypt (native) | Native `bcrypt` is faster but requires C++ compiler — fails in some Vercel builds; bcryptjs is pure JS and always works |
| Auth.js v5 | Custom JWT + iron-session | Auth.js handles the hard parts (cookie encryption, session refresh, middleware integration); custom is more control but more bugs |
**Installation:**
```bash
npx create-next-app@latest teressa-copeland-homes --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd teressa-copeland-homes
npm install next-auth@beta
npm install drizzle-orm @neondatabase/serverless
npm install bcryptjs @vercel/blob
npm install -D drizzle-kit tsx @types/bcryptjs
```
---
## Architecture Patterns
### Recommended Project Structure
```
src/
├── app/
│ ├── agent/
│ │ ├── login/
│ │ │ └── page.tsx # Login page (branded, password toggle, error display)
│ │ └── dashboard/
│ │ └── page.tsx # Post-login landing (blank in Phase 1)
│ ├── api/
│ │ └── auth/
│ │ └── [...nextauth]/
│ │ └── route.ts # Auth.js handler export
│ ├── layout.tsx # Root layout
│ └── page.tsx # Public homepage (placeholder in Phase 1)
├── lib/
│ ├── auth.ts # Auth.js configuration (providers, callbacks, session)
│ └── db/
│ ├── index.ts # Drizzle client initialization
│ └── schema.ts # Database schema (users table only in Phase 1)
├── scripts/
│ └── seed.ts # One-time seed script to create Teressa's account
drizzle/ # Migration files (committed to repo per user decision)
drizzle.config.ts # Drizzle Kit configuration
middleware.ts # Route protection (protects /agent/* routes)
```
### Pattern 1: Auth.js v5 Configuration (JWT Strategy)
**What:** Central auth configuration that exports `handlers`, `auth`, `signIn`, `signOut`. JWT strategy with 7-day rolling expiry. httpOnly encrypted cookie by default.
**When to use:** Phase 1 and every subsequent phase that needs to verify the session.
**Example:**
```typescript
// src/lib/auth.ts
// Source: https://authjs.dev/reference/nextjs
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import bcrypt from "bcryptjs";
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
export const { handlers, auth, signIn, signOut } = NextAuth({
session: {
strategy: "jwt",
maxAge: 7 * 24 * 60 * 60, // 7-day rolling expiry
updateAge: 24 * 60 * 60, // Refresh cookie once per day (not on every request)
},
pages: {
signIn: "/agent/login",
},
providers: [
Credentials({
async authorize(credentials) {
const parsed = loginSchema.safeParse(credentials);
if (!parsed.success) return null;
const [user] = await db
.select()
.from(users)
.where(eq(users.email, parsed.data.email))
.limit(1);
if (!user) return null;
const passwordMatch = await bcrypt.compare(
parsed.data.password,
user.passwordHash
);
if (!passwordMatch) return null;
return { id: user.id, email: user.email };
},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) token.id = user.id;
return token;
},
session({ session, token }) {
session.user.id = token.id as string;
return session;
},
},
});
```
### Pattern 2: Middleware Route Protection
**What:** Protect all `/agent/*` routes at the edge. Unauthenticated requests redirect to `/agent/login`. Does NOT run on static assets or API routes.
**When to use:** Phase 1 only (this file never needs modification in later phases unless new protected route prefixes are added).
**Example:**
```typescript
// middleware.ts (project root, NOT inside src/)
// Source: https://authjs.dev/getting-started/session-management/protecting
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isAgentRoute = req.nextUrl.pathname.startsWith("/agent");
const isLoginPage = req.nextUrl.pathname === "/agent/login";
if (isAgentRoute && !isLoginPage && !isLoggedIn) {
return NextResponse.redirect(new URL("/agent/login", req.nextUrl.origin));
}
// Redirect already-logged-in users away from login page
if (isLoginPage && isLoggedIn) {
return NextResponse.redirect(new URL("/agent/dashboard", req.nextUrl.origin));
}
});
export const config = {
matcher: [
// Match /agent/* but exclude static files and Next.js internals
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};
```
### Pattern 3: Drizzle Client + Schema
**What:** Serverless-safe Drizzle client using Neon HTTP driver. Users table only in Phase 1.
**When to use:** Import `db` from `@/lib/db` in any server component, route handler, or server action.
**Example:**
```typescript
// src/lib/db/index.ts
// Source: https://neon.com/docs/guides/drizzle
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle({ client: sql, schema });
// src/lib/db/schema.ts
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createId } from "@paralleldrive/cuid2"; // or use crypto.randomUUID()
export const users = pgTable("users", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text("email").notNull().unique(),
passwordHash: text("password_hash").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
```
### Pattern 4: Login Form (Server Action)
**What:** Login form that calls the Auth.js `signIn` server action. Displays generic error message. Password toggle for UX.
**When to use:** `/agent/login/page.tsx`
**Example:**
```typescript
// src/app/agent/login/page.tsx (simplified structure)
import { signIn } from "@/lib/auth";
import { AuthError } from "next-auth";
import { redirect } from "next/navigation";
async function loginAction(formData: FormData) {
"use server";
try {
await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirectTo: "/agent/dashboard",
});
} catch (error) {
if (error instanceof AuthError) {
// Return to login with error flag — do NOT re-throw
redirect("/agent/login?error=invalid");
}
throw error; // Re-throw redirects (NEXT_REDIRECT must bubble up)
}
}
```
### Pattern 5: Sign Out Server Action
**What:** Logout from any portal page. Redirects to `/agent/login` with a confirmation message.
**When to use:** Logout button in the portal header/nav.
**Example:**
```typescript
// In a portal layout or header component
import { signOut } from "@/lib/auth";
async function logoutAction() {
"use server";
await signOut({ redirectTo: "/agent/login?signed_out=1" });
}
// On /agent/login page, check for ?signed_out=1 and show the message:
// "You've been signed out."
```
### Pattern 6: Drizzle Migration + Seed Script
**What:** Generate migration files and seed Teressa's account from environment variables.
**When to use:** Once during Phase 1 setup; migrations added in future phases for new tables.
**Example:**
```typescript
// scripts/seed.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import { users } from "../src/lib/db/schema";
import bcrypt from "bcryptjs";
const sql = neon(process.env.DATABASE_URL!);
const db = drizzle({ client: sql });
async function seed() {
const email = process.env.AGENT_EMAIL;
const password = process.env.AGENT_PASSWORD;
if (!email || !password) {
throw new Error("AGENT_EMAIL and AGENT_PASSWORD env vars required");
}
const passwordHash = await bcrypt.hash(password, 12);
await db.insert(users).values({ email, passwordHash })
.onConflictDoNothing(); // Safe to re-run
console.log(`Seeded agent account: ${email}`);
process.exit(0);
}
seed().catch((err) => {
console.error(err);
process.exit(1);
});
```
```json
// package.json scripts section additions:
{
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:seed": "tsx scripts/seed.ts",
"db:studio": "drizzle-kit studio"
}
```
### Anti-Patterns to Avoid
- **Wrapping `signIn`/`signOut` in try/catch without re-throwing:** Auth.js uses `NEXT_REDIRECT` as a control-flow mechanism. Catching it silently swallows the redirect. Only catch `AuthError` — everything else must be re-thrown.
- **Putting `middleware.ts` inside `src/`:** Next.js middleware must live at the project root (same level as `package.json`), not inside `src/`.
- **Using `@auth/drizzle-adapter` when using JWT strategy:** The Drizzle adapter is only needed for database session strategy. JWT strategy does not require an adapter or a sessions table.
- **Exposing `BLOB_READ_WRITE_TOKEN` to client components:** Always call Vercel Blob from server actions or API routes. Never import from client components.
- **Using `bcrypt` (native) instead of `bcryptjs`:** Native bcrypt requires C++ compilation which can fail on Vercel's build system. Always use `bcryptjs` for serverless.
- **Storing `DATABASE_URL` or `AUTH_SECRET` in `.env` committed to repo:** All secrets go through Vercel dashboard only, per user decision.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Session management | Custom JWT encode/decode/cookie logic | Auth.js v5 | Cookie rotation, encryption key management, expiry handling, CSRF protection — all handled; custom implementation has attack vectors |
| Password hashing | Custom hash functions, MD5, SHA-256 alone | bcryptjs | bcrypt is adaptive (salt rounds increase over time), includes automatic salting, designed for passwords specifically |
| Route protection | Manual session checks in every layout/page | Auth.js middleware + `auth()` in layouts | Consistent behavior; middleware runs before page renders so no flash of protected content |
| Database migrations | Manual SQL files, ad-hoc ALTER TABLE | Drizzle Kit generate + migrate | Generates SQL from TypeScript schema diff; migration files are version-controlled |
| Blob URL generation | Custom file storage URLs | Vercel Blob `put()` | CDN distribution, globally cached, secure access controls built in |
**Key insight:** The entire auth domain (sessions, cookies, token rotation, CSRF) has hundreds of edge cases. Auth.js handles them correctly by default — do not re-implement.
---
## Common Pitfalls
### Pitfall 1: NEXT_REDIRECT Swallowed in Try/Catch
**What goes wrong:** `signIn()` throws a `NEXT_REDIRECT` error to perform the redirect after successful login. If wrapped in a catch-all `try/catch`, the redirect never fires — the form appears to do nothing after a correct password.
**Why it happens:** Next.js uses thrown errors (not return values) for redirects and notFound. `AuthError` is a real error; `NEXT_REDIRECT` is a control-flow mechanism masquerading as an error.
**How to avoid:** Only catch `instanceof AuthError`. Re-throw everything else.
**Warning signs:** Login with correct credentials submits but nothing happens; no redirect occurs.
### Pitfall 2: Auth.js v5 Beta Version Instability
**What goes wrong:** `next-auth@beta` is not a stable release. Minor beta updates have broken Credentials provider behavior, session callbacks, and Next.js 16 compatibility.
**Why it happens:** v5 is actively developed; API surface changes between betas.
**How to avoid:** Pin to a specific beta version (e.g., `next-auth@5.0.0-beta.25`). Test after upgrading. Check the [releases page](https://github.com/nextauthjs/next-auth/releases) for breaking changes before updating.
**Warning signs:** Login stops working after `npm update`; TypeScript types change.
### Pitfall 3: Middleware.ts Location
**What goes wrong:** Placing `middleware.ts` inside `src/` causes it to be ignored entirely — no route protection, `/agent/dashboard` is publicly accessible.
**Why it happens:** Next.js middleware must be at the root of the project (same level as `package.json` and `next.config.ts`), not inside `src/`.
**How to avoid:** Always create `middleware.ts` at the project root, not `src/middleware.ts`.
**Warning signs:** Protected routes are accessible without login; no redirects occur.
### Pitfall 4: DATABASE_URL Not Loaded in Production
**What goes wrong:** Neon connection fails in production with "DATABASE_URL is not set" because the env var was only added to `.env.local` and not to Vercel dashboard.
**Why it happens:** `.env.local` is local-only. Vercel requires environment variables set in the dashboard (or via `vercel env add`).
**How to avoid:** Add `DATABASE_URL`, `AUTH_SECRET`, `AGENT_EMAIL`, `AGENT_PASSWORD`, and `BLOB_READ_WRITE_TOKEN` to the Vercel dashboard before first deploy. Run `vercel env pull` locally.
**Warning signs:** Production site works for static pages but fails on any DB-dependent route; Vercel function logs show missing env var.
### Pitfall 5: Drizzle Client Re-initialized on Every Serverless Invocation
**What goes wrong:** Neon connection pool exhaustion in development (not production with HTTP driver, but can affect local PostgreSQL testing).
**Why it happens:** Each serverless function invocation creates a new connection if the `db` client is not cached.
**How to avoid:** Use a module-level singleton in `src/lib/db/index.ts``const sql = neon(...)` at module scope. The Neon HTTP driver is stateless per-request anyway, but the pattern prevents future issues with database adapters.
**Warning signs:** "too many connections" errors in Neon console.
### Pitfall 6: Logout Redirect Without Signed-Out Message
**What goes wrong:** `signOut({ redirectTo: "/agent/login" })` redirects cleanly but shows no confirmation message. The "You've been signed out" message (user requirement) is lost.
**Why it happens:** Auth.js v5 `signOut` does not pass flash messages; there is no built-in toast/flash system.
**How to avoid:** Pass a query parameter: `signOut({ redirectTo: "/agent/login?signed_out=1" })`. On the login page, read `searchParams.signed_out` and render the confirmation message conditionally.
**Warning signs:** Login page shows no confirmation text after logout even though redirect worked.
---
## Code Examples
Verified patterns from official sources:
### Drizzle Config (drizzle.config.ts)
```typescript
// Source: https://neon.com/docs/guides/drizzle
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/lib/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
```
### API Route Handler for Auth.js
```typescript
// src/app/api/auth/[...nextauth]/route.ts
// Source: https://authjs.dev/reference/nextjs
export { GET, POST } from "@/lib/auth";
// (auth.ts exports `handlers`, and { handlers: { GET, POST } } is de-structured here)
// More explicitly:
// import { handlers } from "@/lib/auth";
// export const { GET, POST } = handlers;
```
### Vercel Blob Upload (Server Action)
```typescript
// Source: https://vercel.com/docs/vercel-blob/using-blob-sdk
"use server";
import { put } from "@vercel/blob";
export async function uploadDocument(formData: FormData) {
const file = formData.get("file") as File;
const blob = await put(`documents/templates/${file.name}`, file, {
access: "private", // Documents are never publicly accessible (LEGAL-03)
});
return blob.url;
}
```
### Session Check in Server Component
```typescript
// Source: https://authjs.dev/reference/nextjs
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session) redirect("/agent/login"); // Defense-in-depth beyond middleware
return <div>Welcome, {session.user?.email}</div>;
}
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| NextAuth v4 (`next-auth@4`) | Auth.js v5 (`next-auth@beta`) | 2024-2025 | New unified API across frameworks; App Router native; single `auth()` function replaces `getServerSession()` |
| `pages/api/auth/[...nextauth].ts` | `app/api/auth/[...nextauth]/route.ts` | Next.js 13+ App Router | File location changes; export pattern changes |
| `getServerSideProps` + `getSession` | `await auth()` in Server Components | Next.js 13+ | No more `getServerSideProps`; direct `async/await` in RSC |
| Prisma ORM | Drizzle ORM (preferred for new projects) | 2023-2024 | Drizzle is TypeScript-first, lighter, and has better Neon serverless support |
| `next-auth@4` database sessions | JWT sessions (v5 default) | v5 | Database sessions now require explicit adapter; JWT is simpler for single-user apps |
**Deprecated/outdated:**
- `getServerSession(authOptions)`: replaced by `await auth()` in Auth.js v5
- `pages/api/auth/[...nextauth].js`: replaced by App Router route handler
- `@auth/prisma-adapter` for this project: not needed — using JWT strategy, not database sessions
- `process.env.NEXTAUTH_SECRET`: renamed to `AUTH_SECRET` in v5; both work but v5 uses `AUTH_` prefix
---
## Open Questions
1. **Auth.js v5 exact beta version to pin**
- What we know: `next-auth@beta` points to latest beta; Next.js 16 has a known signIn server action bug with beta.30
- What's unclear: Which beta is most stable with Next.js 15 specifically
- Recommendation: Install `next-auth@beta` initially, note the exact version installed (`npm ls next-auth`), pin it in `package.json`. Test login before declaring Phase 1 complete.
2. **Vercel Blob store creation timing**
- What we know: Store must be created before `BLOB_READ_WRITE_TOKEN` is available; Phase 1 only provisions it, not uses it
- What's unclear: Whether the planner should treat blob provisioning as a separate task or bundled with deployment setup
- Recommendation: Make blob store creation a distinct task in Wave 1 (infrastructure setup) even though it's not exercised until Phase 5.
3. **Teressa's brand assets location**
- What we know: Agent photo is at `/Users/ccopeland/Downloads/red.jpg`; brand colors and logo not yet confirmed
- What's unclear: Whether brand assets will be provided during Phase 1 or if a placeholder design is acceptable
- Recommendation: Build login page with placeholder brand vars (CSS custom properties for colors) so they can be swapped without code changes. Use the photo from the known path.
---
## Sources
### Primary (HIGH confidence)
- [authjs.dev/reference/nextjs](https://authjs.dev/reference/nextjs) — Auth.js v5 Next.js setup, session strategies, middleware pattern
- [authjs.dev/getting-started/session-management/protecting](https://authjs.dev/getting-started/session-management/protecting) — Route protection with `authorized` callback and matcher
- [authjs.dev/concepts/session-strategies](https://authjs.dev/concepts/session-strategies) — JWT vs database session strategies, `maxAge`, `updateAge`
- [neon.com/docs/guides/drizzle](https://neon.com/docs/guides/drizzle) — Neon + Drizzle setup, serverless HTTP driver
- [neon.com/docs/guides/drizzle-migrations](https://neon.com/docs/guides/drizzle-migrations) — Migration workflow with drizzle-kit
- [vercel.com/docs/vercel-blob](https://vercel.com/docs/vercel-blob) — Vercel Blob SDK, `BLOB_READ_WRITE_TOKEN`, store creation
- [npmjs.com/package/bcryptjs](https://www.npmjs.com/package/bcryptjs) — bcryptjs pure JS implementation
### Secondary (MEDIUM confidence)
- [workos.com/blog/nextjs-app-router-authentication-guide-2026](https://workos.com/blog/nextjs-app-router-authentication-guide-2026) — Comprehensive 2026 guide; verified against official Auth.js docs
- [nextjs.org/docs/app/getting-started/installation](https://nextjs.org/docs/app/getting-started/installation) — create-next-app scaffolding options
- [strapi.io/blog/how-to-use-drizzle-orm-with-postgresql-in-a-nextjs-15-project](https://strapi.io/blog/how-to-use-drizzle-orm-with-postgresql-in-a-nextjs-15-project) — Drizzle + Next.js 15 setup tutorial
### Tertiary (LOW confidence — verify during implementation)
- Auth.js v5 beta.25 being "most stable" for Next.js 15 — from community discussion, not official docs; pin and test
- `signOut` UI refresh bug requiring `window.location.reload()` — reported in GitHub discussions, not documented officially
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all packages verified via official docs and npm; versions are current
- Architecture: HIGH — file locations and export patterns verified against Auth.js v5 and Next.js docs
- Pitfalls: MEDIUM-HIGH — most from official docs or verified GitHub issues; NEXT_REDIRECT behavior confirmed in Auth.js docs
**Research date:** 2026-03-19
**Valid until:** 2026-04-18 (30 days — Auth.js beta moves fast; re-verify if planning is delayed more than 2 weeks)