docs(01-foundation): create phase 1 plan
This commit is contained in:
440
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
440
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
@@ -0,0 +1,440 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation/01-RESEARCH.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Contracts this plan creates — downstream plans depend on these exports. -->
|
||||
<!-- Do not deviate from these shapes without updating Plans 02 and 03. -->
|
||||
|
||||
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 });
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Scaffold Next.js 15 project and install all dependencies</name>
|
||||
<files>package.json, tsconfig.json, next.config.ts, tailwind.config.ts, .gitignore</files>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>npm run build passes with no TypeScript compilation errors; npm ls next-auth shows a pinned specific version (not "beta")</verify>
|
||||
<done>Project directory exists, all dependencies installed, exact next-auth version pinned in package.json, npm run build succeeds</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Define database schema, configure Drizzle Kit, and write seed script</name>
|
||||
<files>src/lib/db/schema.ts, src/lib/db/index.ts, drizzle.config.ts, scripts/seed.ts, drizzle/ (generated)</files>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>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)</verify>
|
||||
<done>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</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Configure Auth.js v5 with JWT strategy and create route-protection middleware</name>
|
||||
<files>src/lib/auth.ts, src/app/api/auth/[...nextauth]/route.ts, middleware.ts</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>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/</verify>
|
||||
<done>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</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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))"
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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...")
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md` using the summary template.
|
||||
|
||||
Include in the summary:
|
||||
- Exact next-auth beta version pinned
|
||||
- Whether db:migrate and db:seed were run successfully (or if they are pending user Neon setup)
|
||||
- Location of middleware.ts confirmed (root vs src/)
|
||||
- Any deviations from the planned patterns and why
|
||||
</output>
|
||||
Reference in New Issue
Block a user