---
phase: 01-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- package.json
- tsconfig.json
- next.config.ts
- tailwind.config.ts
- drizzle.config.ts
- src/lib/db/index.ts
- src/lib/db/schema.ts
- src/lib/auth.ts
- middleware.ts
- scripts/seed.ts
- drizzle/
autonomous: true
requirements:
- AUTH-01
- AUTH-02
- AUTH-03
user_setup:
- service: neon
why: "PostgreSQL database for Drizzle ORM"
env_vars:
- name: DATABASE_URL
source: "Neon console → your project → Connection string → Pooled connection (use the ?sslmode=require URL)"
dashboard_config:
- task: "Create a new Neon project named 'teressa-copeland-homes' in the us-east-1 region"
location: "https://console.neon.tech/app/new"
- service: vercel-blob
why: "File/blob storage for signed PDFs and templates (provisioned now, used in Phase 5+)"
env_vars:
- name: BLOB_READ_WRITE_TOKEN
source: "Vercel Dashboard → your project → Storage → Blob → Create Store → token appears after creation"
dashboard_config:
- task: "Create a Blob store named 'teressa-copeland-homes-blob' linked to the Vercel project"
location: "Vercel Dashboard → Storage tab"
- service: vercel
why: "Deployment host and environment variable manager"
env_vars:
- name: AUTH_SECRET
source: "Generate with: npx auth secret — copies to clipboard; paste into Vercel dashboard"
- name: AGENT_EMAIL
source: "Teressa's login email address (e.g. teressa@teressacopelandhomes.com)"
- name: AGENT_PASSWORD
source: "A strong password for Teressa's portal login"
dashboard_config:
- task: "Create a new Vercel project linked to the Git repo; set all 5 env vars (DATABASE_URL, BLOB_READ_WRITE_TOKEN, AUTH_SECRET, AGENT_EMAIL, AGENT_PASSWORD) in the project's Environment Variables settings"
location: "https://vercel.com/new"
must_haves:
truths:
- "Next.js project builds without TypeScript errors (npm run build succeeds)"
- "Drizzle schema generates a valid SQL migration file for the users table"
- "Auth.js configuration exports handlers, auth, signIn, signOut without import errors"
- "Middleware.ts exists at project root (not inside src/) and imports from @/lib/auth"
- "Seed script creates Teressa's hashed-password account in the database when run"
- "Vercel Blob store is provisioned and BLOB_READ_WRITE_TOKEN is available"
artifacts:
- path: "src/lib/db/schema.ts"
provides: "users table definition (id, email, password_hash, created_at)"
exports: ["users"]
- path: "src/lib/db/index.ts"
provides: "Drizzle client singleton using Neon HTTP driver"
exports: ["db"]
- path: "src/lib/auth.ts"
provides: "Auth.js v5 configuration — JWT strategy, Credentials provider, 7-day rolling session"
exports: ["handlers", "auth", "signIn", "signOut"]
- path: "middleware.ts"
provides: "Edge middleware that redirects unauthenticated /agent/* requests to /agent/login"
- path: "scripts/seed.ts"
provides: "One-time seed script that creates Teressa's account from AGENT_EMAIL + AGENT_PASSWORD env vars"
- path: "drizzle.config.ts"
provides: "Drizzle Kit configuration pointing to schema.ts and /drizzle output directory"
- path: "drizzle/"
provides: "Generated SQL migration files committed to repo"
key_links:
- from: "src/lib/auth.ts"
to: "src/lib/db"
via: "Credentials authorize() callback calls db.select(users)"
pattern: "db\\.select.*from.*users"
- from: "middleware.ts"
to: "src/lib/auth.ts"
via: "import { auth } from @/lib/auth"
pattern: "import.*auth.*from.*@/lib/auth"
- from: "scripts/seed.ts"
to: "src/lib/db/schema.ts"
via: "import { users } from ../src/lib/db/schema"
pattern: "import.*users.*from"
---
Scaffold the Next.js 15 project, install all Phase 1 dependencies, define the database schema, configure Drizzle ORM, write the Auth.js v5 configuration with JWT strategy + Credentials provider, create the route-protection middleware, and write the database seed script. This plan produces all the non-UI foundation contracts that Plans 02 and 03 build on top of.
Purpose: Every subsequent plan in this phase and every future phase depends on these files. Getting the contracts right here prevents cascading rework.
Output: Scaffolded project with auth.ts, schema.ts, db/index.ts, middleware.ts, seed.ts, and drizzle migration committed.
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
src/lib/db/schema.ts will export:
```typescript
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(),
});
```
src/lib/auth.ts will export:
```typescript
export const { handlers, auth, signIn, signOut } = NextAuth({ ... });
```
src/lib/db/index.ts will export:
```typescript
export const db = drizzle({ client: sql, schema });
```
Task 1: Scaffold Next.js 15 project and install all dependencies
package.json, tsconfig.json, next.config.ts, tailwind.config.ts, .gitignore
Run the following scaffold command from the parent directory of where the project should live. If the directory `/Users/ccopeland/temp/red/teressa-copeland-homes` already exists, skip scaffolding and install missing deps into the existing project.
```bash
npx create-next-app@latest teressa-copeland-homes \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*" \
--no-git
```
After scaffolding (cd into the project directory), install Phase 1 dependencies:
```bash
# Auth + DB + Blob
npm install next-auth@beta
npm install drizzle-orm @neondatabase/serverless
npm install bcryptjs @vercel/blob zod
npm install -D drizzle-kit tsx @types/bcryptjs dotenv
```
After install, note the exact installed `next-auth` version with `npm ls next-auth` and pin it in package.json (replace `"next-auth": "beta"` with the exact version string like `"next-auth": "5.0.0-beta.25"`).
Add these npm scripts to package.json:
```json
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:seed": "tsx scripts/seed.ts",
"db:studio": "drizzle-kit studio"
```
Ensure `.gitignore` includes:
```
.env
.env.local
.env*.local
```
Do NOT create a `.env` file or `.env.local` file — all secrets go through Vercel dashboard per user decision.
Confirm `npm run build` succeeds (will show only placeholder page errors, not import errors).
npm run build passes with no TypeScript compilation errors; npm ls next-auth shows a pinned specific version (not "beta")
Project directory exists, all dependencies installed, exact next-auth version pinned in package.json, npm run build succeeds
Task 2: Define database schema, configure Drizzle Kit, and write seed script
src/lib/db/schema.ts, src/lib/db/index.ts, drizzle.config.ts, scripts/seed.ts, drizzle/ (generated)
Create `drizzle.config.ts` at the project root:
```typescript
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!,
},
});
```
Create `src/lib/db/schema.ts`:
```typescript
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
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(),
});
```
Create `src/lib/db/index.ts`:
```typescript
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 });
```
Create `scripts/seed.ts`:
```typescript
import "dotenv/config";
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 are required");
}
const passwordHash = await bcrypt.hash(password, 12);
await db.insert(users).values({ email, passwordHash }).onConflictDoNothing();
console.log(`Seeded agent account: ${email}`);
process.exit(0);
}
seed().catch((err) => {
console.error(err);
process.exit(1);
});
```
Generate migration files (requires DATABASE_URL to be set — if not yet available, generate schema only and note that `db:migrate` and `db:seed` run after user sets up Neon):
```bash
# If DATABASE_URL is available locally (via `vercel env pull` after Vercel project creation):
npm run db:generate # Creates drizzle/0000_*.sql migration file
npm run db:migrate # Applies migration to Neon database
npm run db:seed # Creates Teressa's account
# If DATABASE_URL is NOT yet available:
# Generate migration file only (does not need DB connection):
npm run db:generate
# Note in SUMMARY.md that db:migrate and db:seed must run after user sets up Neon
```
The generated `/drizzle` directory with SQL migration files MUST be committed to the repo (per user decision — version-controlled, auditable migrations).
drizzle/ directory contains at least one .sql migration file; src/lib/db/schema.ts and index.ts export users and db without TypeScript errors (npx tsc --noEmit)
Schema file exports users table, db/index.ts exports db singleton, drizzle.config.ts present, seed script present, migration SQL file exists in /drizzle and committed to git
Task 3: Configure Auth.js v5 with JWT strategy and create route-protection middleware
src/lib/auth.ts, src/app/api/auth/[...nextauth]/route.ts, middleware.ts
Create `src/lib/auth.ts` — the single source of truth for authentication configuration:
```typescript
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 days — rolling session per user decision
updateAge: 24 * 60 * 60, // Refresh cookie once per day (not every request)
},
pages: {
signIn: "/agent/login", // Custom login page — per user decision
},
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;
},
},
});
```
Create `src/app/api/auth/[...nextauth]/route.ts`:
```typescript
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
```
Create `middleware.ts` at the PROJECT ROOT (not inside src/ — critical, middleware placed in src/ is silently ignored by Next.js):
```typescript
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";
// Protect all /agent/* routes except the login page itself
if (isAgentRoute && !isLoginPage && !isLoggedIn) {
return NextResponse.redirect(new URL("/agent/login", req.nextUrl.origin));
}
// Redirect already-authenticated users away from the login page
if (isLoginPage && isLoggedIn) {
return NextResponse.redirect(new URL("/agent/dashboard", req.nextUrl.origin));
}
});
export const config = {
matcher: [
// Run on /agent/* routes, excluding Next.js internals and static files
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};
```
Verify `middleware.ts` is at the same level as `package.json` — NOT inside `src/`. This is the single most common pitfall: middleware inside src/ is silently ignored and routes are unprotected.
AUTH_SECRET env var must be set for Auth.js to function. The user will add this to Vercel dashboard. For local development, note this requirement in SUMMARY.md — `vercel env pull` will populate `.env.local` once the Vercel project is created.
npx tsc --noEmit passes with no errors in auth.ts, middleware.ts, and route.ts; middleware.ts file exists at project root (same level as package.json), NOT inside src/
src/lib/auth.ts exports handlers, auth, signIn, signOut; API route handler exists at src/app/api/auth/[...nextauth]/route.ts; middleware.ts exists at project root with /agent/* protection logic
Run these commands from the project root directory after completing all tasks:
```bash
# TypeScript: zero errors across all created files
npx tsc --noEmit
# Build: confirms Next.js can compile the project
npm run build
# Migration file exists (committed to git)
ls drizzle/*.sql
# Middleware location (MUST be at root, not src/)
ls middleware.ts && echo "OK" || echo "MISSING — check location"
ls src/middleware.ts 2>/dev/null && echo "WRONG LOCATION" || echo "Correctly absent from src/"
# Auth exports are importable (no circular deps or missing modules)
node -e "import('./src/lib/auth.ts').then(() => console.log('auth.ts OK')).catch(e => console.error(e.message))"
```
- npm run build succeeds with no TypeScript errors
- drizzle/ directory contains at least one committed SQL migration file
- middleware.ts exists at project root, NOT inside src/
- src/lib/auth.ts exports handlers, auth, signIn, signOut (JWT strategy, 7-day maxAge, Credentials provider)
- src/lib/db/schema.ts exports users table with id, email, passwordHash, createdAt columns
- src/lib/db/index.ts exports db (Drizzle + Neon HTTP driver singleton)
- src/app/api/auth/[...nextauth]/route.ts exports GET and POST
- scripts/seed.ts reads AGENT_EMAIL and AGENT_PASSWORD from env and creates hashed user row
- next-auth version is pinned to a specific beta version (not "beta" or "^5...")