diff --git a/.planning/research/ARCHITECTURE.md b/.planning/research/ARCHITECTURE.md
new file mode 100644
index 0000000..a18e6ff
--- /dev/null
+++ b/.planning/research/ARCHITECTURE.md
@@ -0,0 +1,703 @@
+# Architecture Research
+
+**Domain:** Real estate agent website + PDF document signing web app
+**Researched:** 2026-03-19
+**Confidence:** HIGH
+
+## System Overview
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ teressacopelandhomes.com │
+│ │
+│ ┌──────────────────────────┐ ┌──────────────────────────────────────┐ │
+│ │ PUBLIC SITE │ │ AGENT PORTAL (/agent/*) │ │
+│ │ (unauthenticated) │ │ (authenticated) │ │
+│ │ │ │ │ │
+│ │ - Hero / Bio │ │ - Dashboard │ │
+│ │ - Active Listings │ │ - Client Management │ │
+│ │ - Contact Form │ │ - Document Templates │ │
+│ │ │ │ - PDF Ingest & Field Mapping │ │
+│ └──────────────────────────┘ │ - Signing Request Management │ │
+│ └──────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────────┐ │
+│ │ PDF PIPELINE │ │
+│ │ │ │
+│ │ utahrealestate.com │ │
+│ │ │ │ │
+│ │ ▼ │ │
+│ │ [on-demand fetch / manual upload] │ │
+│ │ │ │ │
+│ │ ▼ │ │
+│ │ [pdfjs-dist: parse → detect form fields / annotation positions] │ │
+│ │ │ │ │
+│ │ ▼ │ │
+│ │ [agent fills fields → pdf-lib writes text into PDF] │ │
+│ │ │ │ │
+│ │ ▼ │ │
+│ │ [agent sends signing link → email with JWT token] │ │
+│ │ │ │ │
+│ │ ▼ │ │
+│ │ [client opens /sign/[token] → draws on canvas → PNG embedded] │ │
+│ │ │ │ │
+│ │ ▼ │ │
+│ │ [signed PDF stored in S3/Vercel Blob → accessible to agent] │ │
+│ └─────────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌──────────────────────┐ ┌─────────────────────┐ ┌─────────────────┐ │
+│ │ PostgreSQL │ │ S3 / Vercel Blob │ │ Email (Resend) │ │
+│ │ (via Prisma) │ │ (PDF storage) │ │ (signing links)│ │
+│ └──────────────────────┘ └─────────────────────┘ └─────────────────┘ │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+## Next.js App Structure
+
+```
+src/
+├── app/
+│ ├── layout.tsx # Root layout (fonts, global styles)
+│ ├── (public)/ # Route group — no URL segment added
+│ │ ├── page.tsx # Home / hero / bio
+│ │ ├── listings/
+│ │ │ └── page.tsx # Active listings (WFRMLS data)
+│ │ └── contact/
+│ │ └── page.tsx # Contact form
+│ ├── (agent)/ # Route group — protected portal
+│ │ ├── layout.tsx # Auth guard + agent shell layout
+│ │ ├── agent/
+│ │ │ ├── dashboard/
+│ │ │ │ └── page.tsx
+│ │ │ ├── clients/
+│ │ │ │ ├── page.tsx # Client list
+│ │ │ │ └── [id]/
+│ │ │ │ └── page.tsx # Client detail + their documents
+│ │ │ └── documents/
+│ │ │ ├── page.tsx # Document template list
+│ │ │ ├── new/
+│ │ │ │ └── page.tsx # Ingest / upload PDF
+│ │ │ └── [id]/
+│ │ │ ├── page.tsx # Document detail / field mapping
+│ │ │ └── send/
+│ │ │ └── page.tsx # Send signing request to client
+│ ├── sign/
+│ │ └── [token]/
+│ │ └── page.tsx # Public signing page (client link)
+│ ├── auth/
+│ │ └── login/
+│ │ └── page.tsx # Agent login page
+│ └── api/
+│ ├── auth/
+│ │ └── [...nextauth]/
+│ │ └── route.ts # NextAuth.js handler
+│ ├── documents/
+│ │ ├── route.ts # List / create documents
+│ │ ├── [id]/
+│ │ │ └── route.ts # Get / update / delete document
+│ │ ├── [id]/ingest/
+│ │ │ └── route.ts # Trigger PDF fetch + parse
+│ │ └── [id]/fill/
+│ │ └── route.ts # Write field values into PDF
+│ ├── sign/
+│ │ ├── [token]/
+│ │ │ └── route.ts # Validate token, serve signing metadata
+│ │ └── [token]/submit/
+│ │ └── route.ts # Accept canvas PNG, embed, finalize
+│ ├── clients/
+│ │ └── route.ts # CRUD for clients
+│ └── listings/
+│ └── route.ts # WFRMLS proxy / cache endpoint
+├── lib/
+│ ├── pdf/
+│ │ ├── parse.ts # pdfjs-dist: extract fields + coordinates
+│ │ ├── fill.ts # pdf-lib: write field values into PDF bytes
+│ │ └── sign.ts # pdf-lib: embed PNG signature at coordinates
+│ ├── email/
+│ │ └── send-signing-link.ts # Resend / Nodemailer: send token link
+│ ├── db/
+│ │ └── prisma.ts # Singleton Prisma client
+│ ├── storage/
+│ │ └── s3.ts # Upload / download / presigned URL helpers
+│ ├── auth/
+│ │ └── session.ts # verifySession() — used in DAL and server actions
+│ └── wfrmls/
+│ └── client.ts # WFRMLS RESO OData API client
+├── components/
+│ ├── public/ # Marketing UI components
+│ ├── agent/ # Agent portal UI components
+│ │ ├── PDFFieldMapper.tsx # Canvas overlay for manual field placement
+│ │ └── DocumentEditor.tsx # Field fill form
+│ └── sign/
+│ └── SignatureCanvas.tsx # react-signature-canvas wrapper (client)
+├── middleware.ts # Edge: check auth cookie, redirect /agent/* to login
+└── prisma/
+ └── schema.prisma
+```
+
+## Component Responsibilities
+
+| Component | Responsibility | Communicates With |
+|-----------|----------------|-------------------|
+| `middleware.ts` | Edge-level auth gate — redirects unauthenticated requests on `/agent/*` to `/auth/login`. Runs on every request. NOT the only auth layer. | Cookie / session token |
+| `(agent)/layout.tsx` | Server Component auth check via `verifySession()` — second auth layer for all agent pages | `lib/auth/session.ts`, redirects to login |
+| `app/api/auth/[...nextauth]/route.ts` | Handles login, session creation, token management via NextAuth.js | PostgreSQL (user table), cookies |
+| `app/api/documents/[id]/ingest/route.ts` | On-demand: fetches PDF from utahrealestate.com or accepts upload, runs parse, stores raw PDF bytes in S3, saves metadata to DB | `lib/pdf/parse.ts`, `lib/storage/s3.ts`, Prisma |
+| `app/api/documents/[id]/fill/route.ts` | Accepts field values from agent, calls `fill.ts`, stores filled PDF in S3 | `lib/pdf/fill.ts`, `lib/storage/s3.ts`, Prisma |
+| `app/api/sign/[token]/route.ts` | Validates JWT token (signature + expiry + jti not used), returns document metadata for rendering | Prisma (SigningRequest), `lib/auth/session.ts` (JWT verify) |
+| `app/api/sign/[token]/submit/route.ts` | Accepts canvas PNG dataURL, calls `sign.ts`, stores signed PDF, marks token as used, writes audit log | `lib/pdf/sign.ts`, `lib/storage/s3.ts`, Prisma |
+| `app/sign/[token]/page.tsx` | Public page — renders PDF via pdfjs-dist, overlays signature fields, sends canvas PNG on submit | `api/sign/[token]`, `SignatureCanvas.tsx` |
+| `lib/pdf/parse.ts` | Extracts existing AcroForm fields and their `[x1, y1, x2, y2]` bounding boxes from PDF bytes using pdfjs-dist. Falls back to empty field list if no form fields exist (manual placement flow). | pdfjs-dist |
+| `lib/pdf/fill.ts` | Loads PDF bytes with pdf-lib, writes agent-supplied text into named form fields (or at coordinates), returns modified PDF bytes | pdf-lib |
+| `lib/pdf/sign.ts` | Embeds signature PNG at specified coordinates on the correct page using `pdfDoc.embedPng()`. Flattens or locks the form. Returns signed PDF bytes. | pdf-lib |
+| `components/agent/PDFFieldMapper.tsx` | Client component — renders PDF page via canvas, lets agent click-drag to define signature area rectangles. Saves field coordinate metadata to DB. | pdfjs-dist (client), `api/documents/[id]` |
+| `lib/wfrmls/client.ts` | Wraps WFRMLS RESO OData API calls. Caches results. Called on-demand from the listings route. | `resoapi.wfrmls.com/reso/odata` |
+
+## PDF Processing Pipeline
+
+Step-by-step: how a PDF goes from utahrealestate.com to a signed document.
+
+```
+utahrealestate.com (or manual upload)
+ │
+ │ [on-demand fetch via lib/wfrmls/client.ts
+ │ OR file upload from agent browser]
+ ▼
+raw PDF bytes
+ │
+ │ [lib/pdf/parse.ts — pdfjs-dist server-side]
+ │ → page.getAnnotations() to find AcroForm widget annotations
+ │ → extract: field name, type, rect [x1, y1, x2, y2], page index
+ │ → if no fields found → flag as "manual placement mode"
+ ▼
+field metadata (JSON stored in Document.fieldMap)
+ │
+ │ [agent fills fields in DocumentEditor.tsx]
+ │ → agent types: property address, client name, price, dates
+ │ → if manual placement mode: agent uses PDFFieldMapper.tsx
+ │ to drag signature areas onto the PDF preview canvas
+ ▼
+field values + coordinates stored in SigningRequest.fieldValues
+ │
+ │ [lib/pdf/fill.ts — pdf-lib server-side]
+ │ → load raw PDF bytes from S3
+ │ → for each field: pdfDoc.getForm().getTextField(name).setText(value)
+ │ → OR: pdfDoc.getPage(n).drawText(value, { x, y, size, font })
+ ▼
+filled PDF bytes → uploaded to S3 as "prepared-{id}.pdf"
+ │
+ │ [agent sends signing request]
+ │ → lib/email/send-signing-link.ts generates JWT token
+ │ → email sent to client with: https://teressacopelandhomes.com/sign/{token}
+ ▼
+client opens /sign/[token]
+ │
+ │ → token validated server-side (api/sign/[token]/route.ts)
+ │ → prepared PDF streamed from S3 and rendered with pdfjs-dist
+ │ → signature field overlays rendered at stored coordinates
+ │ → client draws signature using react-signature-canvas
+ ▼
+client clicks "Submit Signature"
+ │
+ │ → canvas.toDataURL("image/png") sent to api/sign/[token]/submit
+ │ → lib/pdf/sign.ts:
+ │ → pdfDoc.embedPng(signatureBytes)
+ │ → page.drawImage(embeddedPng, { x, y, width, height })
+ │ → pdfDoc.save() → signed PDF bytes
+ │ → signed PDF uploaded to S3 as "signed-{signingRequestId}.pdf"
+ │ → SigningRequest.status → SIGNED, .signedAt → now()
+ │ → SignatureAuditLog entry written (IP, userAgent, timestamp)
+ │ → token's jti marked as used in DB
+ ▼
+signed PDF stored in S3
+ │
+ │ [agent downloads via presigned S3 URL from api/documents/[id]]
+ ▼
+agent receives completed signed document
+```
+
+### Coordinate System Note
+
+PDF.js uses PDF User Space coordinates: origin (0,0) at bottom-left, Y increasing upward, units in points (72 pt/inch). When storing field coordinates from `PDFFieldMapper.tsx` (which uses screen/canvas coordinates), coordinates must be converted from viewport space to PDF user space before storage. pdf-lib uses the same coordinate system as PDF user space.
+
+## Signing Flow (Token-Based)
+
+```
+1. Agent clicks "Send for Signing" on a SigningRequest
+ └─ POST /api/documents/[id]/signing-requests
+
+2. Server generates a JWT with these claims:
+ {
+ sub: signingRequest.id, // the specific request
+ email: client.email, // client who should sign
+ jti: crypto.randomUUID(), // unique token ID for one-time use tracking
+ exp: now + 72 hours, // short TTL
+ iat: now
+ }
+ Signed with HMAC-SHA256 (HS256) using SIGNING_SECRET env var
+
+3. JWT stored as SigningRequest.token (for lookup) and
+ SigningRequest.tokenJti (for one-time use tracking)
+ SigningRequest.expiresAt = now + 72 hours
+
+4. Email sent to client:
+ Subject: "You have a document to sign — Teressa Copeland Homes"
+ Body: https://teressacopelandhomes.com/sign/{encoded-jwt}
+
+5. Client opens link → GET /sign/[token] (public page)
+
+6. Server validates token (api/sign/[token]/route.ts):
+ a. Decode JWT → verify HS256 signature
+ b. Check exp → reject if expired (return 410 Gone)
+ c. Query DB: SigningRequest where token = token AND status = PENDING
+ d. Check SigningRequest.usedAt IS NULL → reject if already used (return 409)
+ e. If valid → return signing metadata (field coordinates, doc info)
+
+7. Client draws signature, clicks submit → POST /api/sign/[token]/submit
+ a. Re-validate token (same checks as step 6)
+ b. Accept PNG dataURL in request body
+ c. Fetch prepared PDF from S3
+ d. Embed PNG at stored coordinates with pdf-lib
+ e. Upload signed PDF to S3
+ f. Update DB: SigningRequest.status = SIGNED, .usedAt = now()
+ g. Write SignatureAuditLog row
+ h. Return 200 → client sees "Thank you, your document has been signed"
+```
+
+**Why not pure database tokens instead of JWT?**
+JWTs allow the server to validate token authenticity without a DB round-trip in step 6a. The DB lookup in 6c still happens to enforce one-time use, but the signature check prevents forged tokens from ever hitting the DB.
+
+**Token URL format:**
+`https://teressacopelandhomes.com/sign/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...`
+The full JWT is the path segment — no query string, no additional encoding needed.
+
+## Key Data Models
+
+```prisma
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+// ─────────────────────────────
+// Auth
+// ─────────────────────────────
+
+model User {
+ id String @id @default(cuid())
+ email String @unique
+ passwordHash String
+ name String
+ role UserRole @default(AGENT)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // NextAuth.js required relations
+ accounts Account[]
+ sessions Session[]
+}
+
+enum UserRole {
+ AGENT
+ ADMIN
+}
+
+// NextAuth.js adapter models (required)
+model Account {
+ id String @id @default(cuid())
+ userId String
+ type String
+ provider String
+ providerAccountId String
+ refresh_token String? @db.Text
+ access_token String? @db.Text
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String? @db.Text
+ session_state String?
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ @@unique([provider, providerAccountId])
+}
+
+model Session {
+ id String @id @default(cuid())
+ sessionToken String @unique
+ userId String
+ expires DateTime
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+}
+
+// ─────────────────────────────
+// Core Domain
+// ─────────────────────────────
+
+model Client {
+ id String @id @default(cuid())
+ name String
+ email String @unique
+ phone String?
+ notes String? @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ signingRequests SigningRequest[]
+}
+
+// A Document is a PDF template — the base form from utahrealestate.com
+// before it is associated with a specific client/transaction.
+model Document {
+ id String @id @default(cuid())
+ name String // e.g. "REPC - Residential"
+ sourceUrl String? // original URL on utahrealestate.com
+ rawS3Key String // S3 key for the original PDF
+ fieldMap Json // parsed form fields: [{ name, type, rect, page }]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ signingRequests SigningRequest[]
+}
+
+// A SigningRequest is a Document sent to a specific Client.
+// The agent fills fields and the client signs.
+model SigningRequest {
+ id String @id @default(cuid())
+ documentId String
+ document Document @relation(fields: [documentId], references: [id])
+ clientId String
+ client Client @relation(fields: [clientId], references: [id])
+
+ // Field values the agent typed in (JSON map of fieldName → value)
+ fieldValues Json @default("{}")
+
+ // Signature field placement (array of { page, x, y, width, height })
+ signatureFields Json @default("[]")
+
+ // S3 keys for each stage of the PDF
+ preparedS3Key String? // after agent fills fields
+ signedS3Key String? // after client signs
+
+ // Signing link (JWT stored for lookup; jti for one-time enforcement)
+ token String? @unique
+ tokenJti String? @unique
+ expiresAt DateTime?
+
+ status SigningStatus @default(DRAFT)
+ sentAt DateTime? // when email was sent
+ viewedAt DateTime? // when client first opened the link
+ signedAt DateTime? // when client completed signing
+ usedAt DateTime? // token consumed (same as signedAt in most cases)
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ auditLogs SignatureAuditLog[]
+}
+
+enum SigningStatus {
+ DRAFT // agent building it, not sent
+ SENT // email sent, awaiting client
+ VIEWED // client opened the link
+ SIGNED // client completed signing
+ EXPIRED // token TTL passed without signing
+ CANCELLED // agent cancelled
+}
+
+// Immutable audit trail for compliance
+model SignatureAuditLog {
+ id String @id @default(cuid())
+ signingRequestId String
+ signingRequest SigningRequest @relation(fields: [signingRequestId], references: [id])
+
+ event AuditEvent
+ actorEmail String // who performed the action
+ ipAddress String?
+ userAgent String?
+ metadata Json? // e.g. { "page": 3, "fieldName": "signature_buyer" }
+
+ createdAt DateTime @default(now())
+
+ @@index([signingRequestId])
+}
+
+enum AuditEvent {
+ REQUEST_CREATED
+ EMAIL_SENT
+ LINK_OPENED
+ SIGNATURE_SUBMITTED
+ SIGNING_COMPLETED
+ TOKEN_EXPIRED
+ REQUEST_CANCELLED
+}
+```
+
+## Authentication Pattern
+
+### Middleware (First Layer — Edge)
+
+`middleware.ts` at the project root runs on every request to `/agent/*`. It checks for the NextAuth.js session cookie (a JWT signed by `NEXTAUTH_SECRET`). If absent or invalid, it redirects to `/auth/login`. This runs at the edge — fast, lightweight, no DB call.
+
+```typescript
+// middleware.ts
+import { withAuth } from "next-auth/middleware";
+
+export default withAuth({
+ pages: { signIn: "/auth/login" },
+});
+
+export const config = {
+ matcher: ["/agent/:path*"],
+};
+```
+
+**IMPORTANT — CVE-2025-29927:** In March 2025, a critical vulnerability was disclosed showing that the `x-middleware-subrequest` header can bypass all Next.js middleware checks. This means middleware alone is NEVER sufficient for auth. Use Next.js 15.2.3+ (patched) AND implement defense-in-depth.
+
+### Layout Auth Guard (Second Layer — Server)
+
+`app/(agent)/layout.tsx` is a Server Component that calls `verifySession()` from `lib/auth/session.ts` on every render. This is the critical second layer that middleware cannot replace. Due to partial rendering, layouts do not re-run on every navigation, but this catch-all guard ensures any direct deep link is still protected.
+
+```typescript
+// app/(agent)/layout.tsx
+import { verifySession } from "@/lib/auth/session";
+import { redirect } from "next/navigation";
+
+export default async function AgentLayout({ children }) {
+ const session = await verifySession(); // throws / returns null if invalid
+ if (!session) redirect("/auth/login");
+ return {children};
+}
+```
+
+### Data Access Layer (Third Layer — Per Data Query)
+
+`lib/auth/session.ts` exports `verifySession()` which calls `getServerSession(authOptions)`. This is called in each Server Component and Route Handler that accesses sensitive data. This ensures auth is checked as close to the data as possible, not just at the routing layer.
+
+```typescript
+// lib/auth/session.ts
+import { getServerSession } from "next-auth";
+import { authOptions } from "@/app/api/auth/[...nextauth]/route";
+import { redirect } from "next/navigation";
+
+export async function verifySession() {
+ const session = await getServerSession(authOptions);
+ if (!session) redirect("/auth/login");
+ return session;
+}
+```
+
+### Anti-Pattern to Avoid
+
+Do NOT return `null` from a layout to hide protected content. Next.js has multiple entry points — a direct URL to a nested route will not necessarily re-run the layout. Always redirect rather than conditionally render.
+
+## File Storage Strategy
+
+### Storage Backend: AWS S3 (or Vercel Blob for simpler setup)
+
+PDFs must NOT be stored on the local filesystem. Vercel (and most serverless platforms) have ephemeral filesystems that are wiped on each deployment. The recommended approach is AWS S3 or Vercel Blob (which is S3-backed).
+
+**S3 Bucket Structure:**
+```
+bucket: teressa-copeland-homes-docs/
+├── raw/
+│ └── {documentId}/original.pdf # untouched PDF from utahrealestate.com
+├── prepared/
+│ └── {signingRequestId}/filled.pdf # agent-filled PDF, awaiting signature
+└── signed/
+ └── {signingRequestId}/signed.pdf # final signed PDF with embedded signature
+```
+
+**Access Pattern — PDFs are NEVER publicly accessible:**
+
+All three PDF stages (raw, prepared, signed) use private S3 ACLs. Access is always mediated through Next.js API routes that verify authentication before generating a time-limited presigned URL.
+
+```typescript
+// lib/storage/s3.ts
+import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
+
+export async function getPresignedDownloadUrl(s3Key: string): Promise {
+ const command = new GetObjectCommand({ Bucket: BUCKET, Key: s3Key });
+ return getSignedUrl(s3Client, command, { expiresIn: 300 }); // 5 min
+}
+```
+
+The signing page (`/sign/[token]`) is special: after the signing token is validated, the Route Handler fetches the prepared PDF from S3 directly and streams it to the client (or returns a short-lived presigned URL for the PDF.js renderer). The S3 URL itself is never exposed to the client.
+
+**For the signing page client-side PDF render:**
+The prepared PDF bytes are fetched server-side and returned as a base64 blob OR the Route Handler returns a short-lived presigned URL that is only passed to the PDF.js worker in memory, not stored in the DOM.
+
+## utahrealestate.com Integration
+
+### What Data Is Available
+
+UtahRealEstate.com (WFRMLS) provides:
+1. **RESO Web API** (OData) at `resoapi.wfrmls.com/reso/odata/` for MLS listing data — property details, photos, prices, status. This requires a licensed vendor account via [vendor.utahrealestate.com](https://vendor.utahrealestate.com/).
+2. **PDF forms** — the real estate transaction forms (REPC, addenda, etc.) are available for download by licensed agents.
+
+### Integration Architecture
+
+```
+ ┌──────────────────────────────────────────┐
+ │ Two Integration Points │
+ └──────────────────────────────────────────┘
+
+1. LISTINGS (public page) 2. PDF FORMS (agent portal)
+ ───────────────────────── ────────────────────────────
+ /api/listings/route.ts Manual or agent-triggered
+
+ On-demand with ISR caching: Agent pastes/enters a URL
+ revalidate = 3600 (1 hour) to a form PDF or uploads
+ one directly from their
+ WFRMLS RESO OData API: computer.
+ → Filter: ListAgentEmail = agent
+ → Returns: address, price, The /api/documents/[id]/ingest
+ photos, status, beds/baths route handler:
+ 1. Fetches the PDF (if URL)
+ Cached in Next.js fetch cache 2. Stores raw bytes in S3
+ or Redis for 1 hour. 3. Runs pdfjs-dist parse
+ 4. Stores field map in DB
+```
+
+### Why On-Demand (Not Scheduled Batch) for Forms
+
+The agent workflow is: "I have a specific transaction, I need this specific form." The PDF forms are pulled once per document template, not on a schedule. There is no need for a job queue or cron for the PDF ingest path — it is a standard HTTP request triggered by the agent clicking "Import Form."
+
+For listings, Next.js ISR (Incremental Static Regeneration) with `revalidate` on the fetch call is the right tool: the public listings page rebuilds at most once per hour, keeping WFRMLS API calls minimal.
+
+**No background job queue (BullMQ/Redis) is needed for this use case.** A job queue would be warranted if PDFs required multi-minute processing (OCR, AI field detection), but pdf-lib + pdfjs-dist parsing completes in under 2 seconds for typical real estate forms. If OCR is added later (for scanned PDFs), that is the right time to introduce BullMQ.
+
+### WFRMLS API Client
+
+```typescript
+// lib/wfrmls/client.ts
+const BASE = "https://resoapi.wfrmls.com/reso/odata";
+
+export async function fetchAgentListings(agentEmail: string) {
+ const res = await fetch(
+ `${BASE}/Property?$filter=ListAgentEmail eq '${agentEmail}' and StandardStatus eq 'Active'&$select=ListingKey,UnparsedAddress,ListPrice,BedroomsTotal,BathroomsTotalInteger,Media`,
+ {
+ headers: { Authorization: `Bearer ${process.env.WFRMLS_API_TOKEN}` },
+ next: { revalidate: 3600 }, // ISR — rebuild at most hourly
+ }
+ );
+ return res.json();
+}
+```
+
+## Anti-Patterns
+
+**1. Middleware-only auth (CVE-2025-29927)**
+Relying solely on `middleware.ts` for authentication was demonstrated to be exploitable in March 2025 via the `x-middleware-subrequest` header bypass. Always add a second auth check in the layout Server Component and a third in every Route Handler or Server Action that accesses private data.
+
+**2. Storing PDFs on the local filesystem**
+`/tmp` on serverless platforms is ephemeral and size-limited. Even in self-hosted setups, local storage creates scaling and durability problems. All PDFs must go through S3/Blob from day one.
+
+**3. Sending the S3 presigned URL to the browser for the signed PDF**
+The presigned URL for the agent-accessible signed PDF should be generated server-side and returned only to authenticated agent sessions. Never include presigned URLs in publicly accessible API responses.
+
+**4. Client-side-only auth checks**
+Hiding UI based on `useSession()` in a client component is cosmetic only. The user can open DevTools and call the API directly. Every API Route Handler and Server Action must independently verify the session.
+
+**5. Using `null`-return in layouts to block content**
+Due to partial rendering, a layout only re-runs when its segment changes. A user navigating within the agent portal may skip the layout re-run. Always `redirect()` to the login page instead of returning null.
+
+**6. Storing signed PDF URLs in the SigningRequest and serving them publicly**
+The signed PDF is a legal document. It must never be served without authentication. Use the S3 key in the DB and generate presigned URLs only in authenticated route handlers.
+
+**7. Treating the signing token as a long-lived credential**
+Signing tokens should expire in 48–72 hours and be invalidated after first use. A client who loses the email should request a new link from the agent, not be able to reuse an old token.
+
+**8. Embedding signature as a text glyph instead of an image**
+Some implementations write the signature as a font character. This is legally weaker than embedding the actual PNG image drawn by the client's hand. Use `pdfDoc.embedPng()` with pdf-lib to embed the real canvas output.
+
+**9. Running pdfjs-dist without SSR disabled**
+pdfjs-dist relies on canvas and Web Workers. In Next.js, any component that uses it for rendering must be a Client Component with `dynamic(() => import(...), { ssr: false })`. The server-side parse in `lib/pdf/parse.ts` uses the Node.js legacy build (`pdfjs-dist/legacy/build/pdf.mjs`) which does not require a worker.
+
+**10. Skipping the audit log**
+Real estate document signing has legal weight. Every event — link sent, link opened, signature submitted — must be recorded with IP address, user agent, and timestamp in `SignatureAuditLog`. This table is append-only and should never be updated or deleted.
+
+## Build Order
+
+Components must be built in dependency order. Each phase unblocks the next.
+
+```
+Phase 1 — Foundation (no deps)
+├── Prisma schema + migrations
+├── PostgreSQL setup
+├── S3 bucket + IAM policy
+├── NextAuth.js credentials provider
+└── Middleware + layout auth guard
+
+Phase 2 — Public Site (depends on Phase 1)
+├── Public layout + home page (hero, bio)
+├── Contact form + API route
+└── WFRMLS client + listings page (ISR)
+
+Phase 3 — Agent Shell (depends on Phase 1)
+├── Agent dashboard (skeleton)
+├── Client CRUD (list, create, edit)
+└── Agent login page
+
+Phase 4 — PDF Ingest (depends on Phase 1, 3)
+├── lib/storage/s3.ts (upload/download/presign)
+├── lib/pdf/parse.ts (pdfjs-dist field extraction)
+├── Document create + ingest API route
+└── Document list + detail pages
+
+Phase 5 — PDF Fill + Field Mapping (depends on Phase 4)
+├── lib/pdf/fill.ts (pdf-lib field writing)
+├── PDFFieldMapper.tsx (canvas overlay, manual placement)
+├── DocumentEditor.tsx (field value form)
+└── Fill API route
+
+Phase 6 — Signing Flow (depends on Phase 5)
+├── JWT token generation + storage in SigningRequest
+├── lib/email/send-signing-link.ts
+├── Send signing request UI + API
+├── /sign/[token] public page
+├── SignatureCanvas.tsx (react-signature-canvas)
+├── lib/pdf/sign.ts (pdf-lib PNG embed)
+├── Submit signature API route
+└── Token validation + one-time use enforcement
+
+Phase 7 — Audit + Download (depends on Phase 6)
+├── SignatureAuditLog writes in all sign endpoints
+├── Agent document download (presigned S3 URL)
+└── Signing status tracking in agent portal
+```
+
+## Sources
+
+- [Next.js Authentication Guide (WorkOS) 2026](https://workos.com/blog/nextjs-app-router-authentication-guide-2026)
+- [Next.js App Router Route Groups](https://nextjs.org/docs/app/api-reference/file-conventions/route-groups)
+- [Next.js Official Authentication Guide](https://nextjs.org/docs/app/building-your-application/authentication)
+- [Auth.js — Protecting Routes](https://authjs.dev/getting-started/session-management/protecting)
+- [CVE-2025-29927 — Next.js Middleware Bypass (Vercel Postmortem)](https://nextjs.org/blog/cve-2025-29927)
+- [CVE-2025-29927 Technical Analysis (ProjectDiscovery)](https://projectdiscovery.io/blog/nextjs-middleware-authorization-bypass)
+- [Datadog Security Labs — Next.js Middleware Auth Bypass](https://securitylabs.datadoghq.com/articles/nextjs-middleware-auth-bypass/)
+- [pdf-lib — Create and Modify PDF Documents](https://pdf-lib.js.org/)
+- [pdfjs-dist on npm](https://www.npmjs.com/package/pdfjs-dist)
+- [Read and Parse PDFs with pdfjs, Create with pdf-lib (Liran Tal)](https://lirantal.com/blog/how-to-read-and-parse-pdfs-pdfjs-create-pdfs-pdf-lib-nodejs)
+- [PDF.js Annotations and Forms (DeepWiki)](https://deepwiki.com/mozilla/pdfjs-dist/5.4-annotations-and-forms)
+- [Prisma Schema Data Models](https://www.prisma.io/docs/orm/prisma-schema/data-model/models)
+- [Implementing Entity Audit Log with Prisma](https://medium.com/@gayanper/implementing-entity-audit-log-with-prisma-9cd3c15f6b8e)
+- [JWT Best Practices (Curity)](https://curity.io/resources/learn/jwt-best-practices/)
+- [JWT Introduction — jwt.io](https://www.jwt.io/introduction)
+- [AWS S3 + Next.js — Upload and Save References in Postgres (Neon)](https://neon.com/guides/next-upload-aws-s3)
+- [Vercel Blob Storage](https://vercel.com/kb/guide/how-can-i-use-aws-s3-with-vercel)
+- [BullMQ with Next.js](https://medium.com/@asanka_l/integrating-bullmq-with-nextjs-typescript-f41cca347ef8)
+- [UtahRealEstate.com Vendor / Developer Portal](https://vendor.utahrealestate.com/)
+- [WFRMLS RESO OData API Examples](https://www.reso.org/web-api-examples/mls/utah-mls/)
+- [Next.js Middleware Authentication — HashBuilds](https://www.hashbuilds.com/articles/next-js-middleware-authentication-protecting-routes-in-2025)
+- [Next.js 15 Folder Structure Guide (jigz.dev)](https://www.jigz.dev/blogs/how-to-organize-next-js-15-app-router-folder-structure)
+- [Secure Routes in Next.js (freeCodeCamp)](https://www.freecodecamp.org/news/secure-routes-in-next-js/)
+- [Next.js Route Groups — Medium](https://medium.com/@shrutishende11/next-js-route-groups-organizing-your-app-router-like-a-pro-aa58ca11f973)
+- [PDF Page Coordinates (pdfscripting.com)](https://www.pdfscripting.com/public/PDF-Page-Coordinates.cfm)
+
+---
+*Architecture research for: Real estate agent website + document signing*
+*Researched: 2026-03-19*
diff --git a/.planning/research/FEATURES.md b/.planning/research/FEATURES.md
new file mode 100644
index 0000000..b6618f9
--- /dev/null
+++ b/.planning/research/FEATURES.md
@@ -0,0 +1,289 @@
+# Feature Research
+
+**Domain:** Real estate agent website + document signing portal
+**Researched:** 2026-03-19
+**Confidence:** HIGH — cross-referenced multiple industry sources across both the marketing site and the document workflow sides
+
+## Feature Landscape
+
+### Table Stakes (Users Expect These)
+
+Features users assume exist. Missing these = product feels incomplete.
+
+| Feature | Why Expected | Complexity | Notes |
+|---------|--------------|------------|-------|
+| Professional hero section with agent photo and bio | Every real estate site has this; first impression of trust and warmth | LOW | Teressa's photo is provided; needs warm/approachable design treatment |
+| Mobile-responsive design | 60%+ of real estate searches happen on mobile; non-mobile sites are invisible | MEDIUM | Next.js with Tailwind handles this; must test signing flow on mobile |
+| Active property listings display | Clients expect to see what you're selling; core proof of business | HIGH | WFRMLS/utahrealestate.com integration; no guaranteed public API — may require credential-based scraping |
+| Contact form / call to action | Clients need a way to reach out; missing CTA = lost leads | LOW | Simple form with email delivery; Resend or similar service |
+| Client testimonials / social proof | Trust signal; real estate is relationship-driven | LOW | Static content initially; agent provides testimonial copy |
+| Agent login with secure authentication | Portal requires identity protection; any breach exposes client documents | MEDIUM | Next-Auth or similar; email magic link or password auth |
+| Document list / client management dashboard | Agent must see all active clients and document statuses at a glance | MEDIUM | CRUD operations on documents and clients; status tracking (draft, sent, signed) |
+| PDF rendering in browser | Agent must see the actual document before sending | HIGH | PDF.js or similar; must render Utah standard forms accurately |
+| Signature field placement on PDF | Core document workflow; agent must designate where client signs | HIGH | Drag-and-drop UI on PDF canvas; position stored as x/y/page coordinates |
+| Email delivery of signing link to client | How client receives the document; no email = no signing | MEDIUM | Unique tokenized URL per document; Resend or Postmark |
+| Client signing experience (no account) | Clients will not create accounts; friction here loses signatures | HIGH | Anonymous token-based access; canvas signature capture; mobile-friendly |
+| Signed document storage and retrieval | Agent must be able to access signed documents after the fact | MEDIUM | Secure file storage (S3 or similar); associated with client record |
+| Audit trail (IP, timestamp, signature image) | Legal requirement under ESIGN Act and UETA for enforceability | HIGH | Must capture: signer IP address, timestamp, user agent, drawn signature image embedded into PDF |
+| Tamper-evident record after signing | Legal requirement; document must be provably unmodified after signing | HIGH | PDF hash or cryptographic seal after signature embed; store original + signed versions |
+
+### Differentiators (Competitive Advantage)
+
+Features that set the product apart. Not valuable to every agent, but meaningful here.
+
+| Feature | Value Proposition | Complexity | Notes |
+|---------|-------------------|------------|-------|
+| Custom branded signing experience | DocuSign and HelloSign look like DocuSign and HelloSign; this looks like Teressa's business | MEDIUM | Branded email, branded signing page, Teressa's colors/logo throughout |
+| No client account required | Zero friction for clients — one link, one click, one signature; competitors require accounts | LOW | Token in URL grants access; no login wall |
+| Agent-fills-then-client-signs workflow | Teressa fills property/client details before sending; client only signs — matches how real estate actually works | MEDIUM | Two-phase form flow: agent prep mode vs. client sign mode |
+| Forms library import from utahrealestate.com | Teressa already has access and uses these forms; importing avoids manual re-entry | HIGH | Session-based auth to utahrealestate.com; parse/download available forms; legal consideration: forms may be member-only |
+| Heuristic signature field detection | Auto-detect likely signature zones on Utah standard forms; reduce agent setup time | HIGH | Pattern matching on PDF text/whitespace; Utah forms have predictable structure; manual override always available |
+| Document status tracking (sent/viewed/signed) | Agent knows if client opened the link; can follow up proactively | MEDIUM | Link open tracking via redirect; signed confirmation webhook/callback |
+| Hyper-local SEO content structure | Neighborhood guides and local market content rank for Utah-specific searches | LOW | Content structure in Next.js; agent provides copy; builds over time |
+| Listings tied to agent brand | Listings display under Teressa's brand, not a third-party portal | HIGH | WFRMLS feed integration; listing detail pages on teressacopelandhomes.com |
+
+### Anti-Features (Commonly Requested, Often Problematic)
+
+Features that seem good but create problems.
+
+| Feature | Why Requested | Why Problematic | Alternative |
+|---------|---------------|-----------------|-------------|
+| Client login portal / accounts | "Clients should be able to check document status" | Adds auth complexity, email verification flows, password resets, and security surface; clients rarely return to portals; v1 is one-time signing | Email-based status updates when document is signed; agent dashboard is source of truth |
+| Notification/reminder automation for unsigned documents | "I want automatic follow-ups" | Requires scheduling infrastructure (cron/queue), unsubscribe handling, deliverability management; high complexity for low-frequency use | Manual reminder workflow; agent sees unsigned status in dashboard and sends manual follow-up email; add automation only when confirmed as real pain point |
+| Multi-agent / team support | "What if I hire someone?" | Adds role/permissions model, shared document ownership, audit trails per agent; doubles auth complexity | Solo agent model only in v1; revisit if business grows |
+| Native iOS/Android app | "Clients prefer apps" | Separate codebase, app store approval, push notification infrastructure; signing on mobile web is fully viable | Responsive PWA-quality mobile web; signing canvas works in mobile browsers; over 50% of e-signatures now happen on mobile web successfully |
+| Real-time collaboration / live signing sessions | "Sign together on a video call" | WebSocket infrastructure, session coordination, conflict resolution; extreme complexity for rare use case | Async signing via link is industry standard; agents schedule signing calls separately |
+| Mortgage calculator / affordability tools | Common on real estate sites | Third-party data dependency; not core to Teressa's workflow; dilutes site focus | Link to trusted external tools (CFPB mortgage calculator) |
+| Full IDX search with saved searches and alerts | "Clients want to browse all listings" | Full IDX integration requires WFRMLS enrollment ($50 + ~$10/month data feed via approved vendor), saved searches need client accounts, and email automation; overkill for a personal agent site | Display Teressa's active listings only; clients searching the broader Utah market use utahrealestate.com, Zillow, or Redfin |
+| AI chatbot | "24/7 lead capture" | Adds LLM costs, prompt engineering, hallucination risk for legal/real estate queries; for a solo agent with a personal brand, a chatbot feels impersonal and off-brand | Clear contact form; visible phone number; fast personal email response is the real differentiator |
+| DocuSign/HelloSign integration | "Just use an existing service" | $25-50+/month recurring for a solo agent; loses brand control; client experience carries third-party branding; 97% of agents already use e-sig but that means DocuSign-fatigue is real | Custom in-house signature capture (already decided); lower cost, full brand control, Teressa-branded experience |
+| Blockchain/smart contract signing | Sounds modern | Zero adoption in Utah residential real estate; no tooling the industry accepts; legal standing unclear in Utah courts | Standard ESIGN/UETA-compliant audit trail is legally sufficient and well understood by Utah courts |
+| Blog / content marketing hub | SEO traffic over time | Meaningful payoff requires 6-12+ months of consistent publishing; solo agent rarely has bandwidth; abandoned blog hurts credibility | One strong neighborhood page is worth more than a dozen generic posts; defer until cadence is proven |
+| In-app PDF editing (content editing, not field placement) | "Fix typos in the form before sending" | Real estate contracts have legally mandated language; editing form content creates liability and REALTORS association compliance issues | Treat PDFs as read-only containers; only add/position signing fields on top; edit the source form in utahrealestate.com if needed |
+| SMS / text signing notifications | "Higher open rates than email" | Requires phone number collection, TCPA compliance, SMS provider setup and per-message cost; adds friction to the sending flow | Email-only is sufficient; clients are conditioned to DocuSign-style email delivery; revisit only if open rates prove to be a problem |
+
+## Feature Dependencies
+
+```
+[Agent Login]
+ └──requires──> [Secure Auth (Next-Auth or similar)]
+ └──enables──> [Agent Dashboard]
+ └──enables──> [Client Management]
+ └──enables──> [Document Workflow]
+
+[Document Workflow]
+ └──requires──> [PDF Rendering in Browser]
+ └──requires──> [Signature Field Placement UI]
+ └──requires──> [File Storage (S3)]
+ └──requires──> [Email Delivery Service]
+ └──enables──> [Client Signing Link]
+ └──requires──> [Token-Based Anonymous Access]
+ └──requires──> [Canvas Signature Capture]
+ └──requires──> [Audit Trail Capture (IP + timestamp)]
+ └──enables──> [PDF Signature Embed]
+ └──enables──> [Signed Document Storage]
+
+[Listings Display]
+ └──requires──> [WFRMLS/utahrealestate.com Integration]
+ └──independent of──> [Document Workflow]
+
+[Forms Library Import]
+ └──requires──> [utahrealestate.com credential-based session]
+ └──enhances──> [Document Workflow] (pre-populated PDFs)
+
+[Heuristic Field Detection]
+ └──enhances──> [Signature Field Placement UI]
+ └──independent of──> [Canvas Signature Capture]
+
+[Document Status Tracking]
+ └──requires──> [Email Delivery Service]
+ └──enhances──> [Agent Dashboard]
+```
+
+### Dependency Notes
+
+- **Document Workflow requires PDF Rendering:** The agent must see the document to place fields. PDF.js is the standard browser-side renderer; server-side PDF manipulation (pdf-lib or PDFKit) is needed for embedding signatures.
+- **Client Signing requires Token-Based Access:** The token in the email link is the client's identity. It must be single-use or expiring; no account required.
+- **Audit Trail requires Canvas Signature Capture:** The signature image, IP, and timestamp are all captured at the moment of signing; they must be written together atomically.
+- **Listings Display is independent of Document Workflow:** These are two separate subsystems that share only the marketing site shell. WFRMLS integration can be built or skipped without affecting document signing.
+- **Forms Library Import enhances but does not block Document Workflow:** The agent can manually upload PDFs if import fails. Import is a convenience, not a prerequisite.
+- **Heuristic Field Detection enhances Placement UI:** It pre-populates suggested field positions. Agent always has final control. Failure of detection degrades gracefully — agent places fields manually.
+
+## MVP Definition
+
+### Launch With (v1)
+
+Minimum viable product — what's needed to validate the concept.
+
+- [ ] Marketing site with agent photo, bio, contact form, and testimonials — establishes professional presence
+- [ ] Active listings pulled from WFRMLS/utahrealestate.com and displayed on site — proves integration works
+- [ ] Agent login — gates the document portal
+- [ ] Client management: create/edit clients with name and email — minimum data model
+- [ ] PDF upload and browser rendering — agent can see the form
+- [ ] Signature field placement UI (drag-and-drop on PDF canvas) — core document prep workflow
+- [ ] Email delivery of unique signing link to client — how client gets the document
+- [ ] Token-based anonymous client signing page — client opens link, sees PDF, draws signature
+- [ ] Audit trail capture: IP, timestamp, drawn signature image — legal requirement
+- [ ] PDF signature embed and signed document storage — completes the sign-and-store loop
+- [ ] Agent dashboard showing document status (draft / sent / signed) — agent knows what's done
+
+### Add After Validation (v1.x)
+
+Features to add once core is working.
+
+- [ ] Forms library import from utahrealestate.com — eliminates manual PDF upload step; add when upload friction is confirmed as a pain point
+- [ ] Heuristic signature field detection on Utah standard forms — reduces agent prep time; add when field placement is confirmed as the biggest time cost
+- [ ] Document status tracking (link opened / viewed) — add when agents ask "did my client open it?"
+- [ ] Signed document download for client (PDF link in confirmation email) — add when clients report needing their copy
+- [ ] Multiple signature fields per document (initials, date fields, checkboxes) — add when agents hit limits of single-signature flow
+
+### Future Consideration (v2+)
+
+Features to defer until product-market fit is established.
+
+- [ ] Notification/reminder system for unsigned documents — requires scheduling infrastructure; defer until manual follow-up is confirmed as painful
+- [ ] Bulk document send (same form to multiple clients) — edge case for v1 solo agent use; revisit if volume grows
+- [ ] Agent-side annotation / markup on PDFs (comments, notes to client) — nice to have; adds complexity to PDF rendering pipeline
+- [ ] Client portal with document history — requires client accounts; out of scope for v1 by design
+- [ ] Multi-agent / brokerage support — business model expansion; not current scope
+
+## Feature Prioritization Matrix
+
+| Feature | User Value | Implementation Cost | Priority |
+|---------|------------|---------------------|----------|
+| Marketing site (photo, bio, CTA) | HIGH | LOW | P1 |
+| Active listings display (WFRMLS) | HIGH | HIGH | P1 |
+| Agent login / auth | HIGH | MEDIUM | P1 |
+| Client management (CRUD) | HIGH | LOW | P1 |
+| PDF upload and browser rendering | HIGH | MEDIUM | P1 |
+| Signature field placement UI | HIGH | HIGH | P1 |
+| Email delivery of signing link | HIGH | MEDIUM | P1 |
+| Token-based anonymous client signing | HIGH | MEDIUM | P1 |
+| Canvas signature capture | HIGH | MEDIUM | P1 |
+| Audit trail (IP + timestamp + image) | HIGH | MEDIUM | P1 |
+| PDF signature embed | HIGH | HIGH | P1 |
+| Signed document storage | HIGH | MEDIUM | P1 |
+| Agent dashboard / document status | HIGH | MEDIUM | P1 |
+| Forms library import (utahrealestate.com) | MEDIUM | HIGH | P2 |
+| Heuristic field detection | MEDIUM | HIGH | P2 |
+| Document open/view tracking | MEDIUM | LOW | P2 |
+| Signed document email to client | MEDIUM | LOW | P2 |
+| Multiple field types (initials, date, checkbox) | MEDIUM | MEDIUM | P2 |
+| Neighborhood guides / SEO content | LOW | LOW | P2 |
+| Unsigned document reminders | LOW | HIGH | P3 |
+| Client portal with document history | LOW | HIGH | P3 |
+| Bulk document sending | LOW | MEDIUM | P3 |
+| PDF annotation / markup | LOW | HIGH | P3 |
+
+**Priority key:**
+- P1: Must have for launch
+- P2: Should have, add when possible
+- P3: Nice to have, future consideration
+
+## Real Estate Agent Site Specifics
+
+### What Makes Real Estate Agent Sites Work
+
+**The hero section is the trust handshake.** Website visitors form an opinion in under one second. For a solo agent like Teressa, the hero must include: a warm, professional photo (not stock), a one-sentence value proposition, and a primary CTA ("See My Listings" or "Get in Touch"). The photo choice matters — action shots or candid poses perform better than formal headshots at conveying approachability. Refresh every 18-24 months.
+
+**Bio should lead with client benefit, not credential list.** The most common real estate bio mistake is listing designations and years of experience. The bio's job is lead generation. It should speak directly to the reader's situation, include one specific achievement or number, and close with a call to action. 150-200 words is right — enough to establish trust, short enough to actually be read. Avoid AI-generated copy; it reads as boilerplate and undermines the personal brand.
+
+**Testimonials belong on the homepage, not only on a reviews page.** 88% of consumers trust online reviews as much as personal recommendations. Testimonials with client photos outperform text-only significantly. Recency matters — old testimonials hurt. The most powerful placement is: one or two short pull quotes below the hero, and a fuller testimonials section further down the page. Sourcing from Google or Zillow reviews adds third-party credibility.
+
+**Listings are proof of work.** Displaying Teressa's active listings is a trust signal as much as a utility. It says: this agent is active, this site is current, this person knows the market. Each listing card should show: property photo, price, address, beds/baths/sqft, and a link to the detail page. WFRMLS covers 97% of Utah listings. Enrollment costs $50 one-time + ~$10/month for the data feed; approved vendors include IDX Broker, Showcase IDX, and Realtyna.
+
+**Contact friction kills conversions.** The contact form should ask for three fields: name, email, message. Every additional field reduces submission rate. Phone number should be visible in the site header — real estate clients often prefer to call. A "schedule a call" link (Calendly or equivalent) converts higher than an open-ended message form.
+
+**Warm/approachable is the brief; make it specific.** This is not a corporate brokerage site. The design should feel like it belongs specifically to Teressa — her colors, her copy voice, her photo — not Real Estate Website Template #47. Soft palette, generous white space, readable typography, and photography that shows Teressa as a real person create the brand differentiation that generic IDX portal sites can't compete with.
+
+**Mobile is the primary device.** Over 60-70% of real estate web traffic is mobile. The listings grid, the bio, the contact form, and critically the signing flow must all work flawlessly on a phone. iOS Safari and Android Chrome are the primary targets. Test all touch interactions, font sizes, and image loading on actual devices.
+
+---
+
+## Document Signing UX Specifics
+
+### What Makes E-Signature Flows Work
+
+**No client account creation — ever.** This is the single most important UX decision for the signing flow. The client clicks a link in an email and signs. Every additional step (create account, set password, verify email separately) is an abandonment driver. The unique signing URL is the authentication. This is how DocuSign, HelloSign, and every mainstream e-sig platform actually works for signers.
+
+**Show where to sign immediately.** When the client opens the signing link, the first thing they should see is the document with the first unsigned field highlighted — not an explainer screen, not a terms acceptance wall. Signature fields should be visually distinct (yellow or blue highlighted box is the industry convention). Auto-scroll to the next unsigned field after each completion.
+
+**Show progress through the document.** Clients signing a multi-page real estate contract don't know how long it takes. "Field 3 of 7" or a page indicator sets expectations and reduces abandonment. When clients can see they're almost done, they finish. Studies show streamlined signing UIs boost completion rates by up to 30%.
+
+**Canvas signing must work well on mobile.** Over 50% of business signatures now happen on mobile. The canvas element must: be large enough to sign on a phone screen, respond correctly to touch events on iOS Safari (which handles touch differently than Chrome), provide a clear "Clear" button to redo, and produce a legible result. Always offer "type your name" as a fallback — not everyone is comfortable drawing on a phone.
+
+**The confirmation screen closes the loop.** After the last field is signed and submitted, the client needs: (1) an explicit success message, (2) confirmation that a copy is coming by email, and (3) optionally a download button. This is the moment the client feels done — ambiguity here creates follow-up calls.
+
+**Agent-side field placement must be fast.** If placing fields takes more than 2-3 minutes per document, it becomes a daily friction point. Best UX: drag field types from a sidebar onto the PDF; resize with handles; delete with the Delete key or a trash icon. Auto-scroll across pages. Support at minimum: signature, initials, date (auto-populated at signing time), text input, checkbox. A "save this field layout as a template" option eliminates the most common repetitive task — Utah standard forms don't change.
+
+**Status dashboard answers the one question agents ask daily: "Did they sign yet?"** A simple list of documents with status badges (Draft, Sent, Viewed, Signed) and the last-activity timestamp answers this without digging into individual records. A one-click "Resend Link" or "Send Reminder" button belongs here — not buried in a document detail view. Best practice reminder cadence: Day 1 (initial send), Day 3, Day 5, Day 7 (escalate to a call).
+
+**Audit trail is quiet but legally essential.** The agent should never need to think about this during normal operation. But every signed document must silently record: signing timestamp, client IP address, email address used, and the drawn signature image embedded into the PDF. This data protects Teressa in any dispute. The signed document itself plus a basic certificate are what Utah courts understand and accept under ESIGN/UETA.
+
+**Sub-3-second load time for the signing page.** Slow-loading signing pages erode trust — the client wonders if the link is legitimate. Optimize the PDF viewer initialization, use lazy loading for pages beyond the first, and keep the JavaScript bundle lean. A 3-second or faster load is the industry target.
+
+---
+
+## Competitor Feature Analysis
+
+| Feature | DocuSign Rooms | HelloSign / Dropbox Sign | Lone Wolf Authentisign | Our Approach |
+|---------|---------------|--------------------------|------------------------|--------------|
+| E-signature capture | Canvas + typed + uploaded | Canvas + typed | Canvas | Canvas-drawn only (v1); matches real estate norm |
+| Audit trail | Full Certificate of Completion (IP, timestamp, actions) | Audit Report (IP, timestamp) | Basic trail | IP + timestamp + signature image; ESIGN/UETA compliant |
+| Client account required | Yes | Yes | Yes | No — anonymous token link; differentiator |
+| Branding | DocuSign branding | Dropbox branding | Lone Wolf branding | Teressa's brand throughout |
+| Forms library | Generic; no Utah MLS integration | Generic | MLS-connected | utahrealestate.com import (v1.x); Utah-specific |
+| Monthly cost | $25-50+/month | $15-25+/month | MLS membership fee | $0 incremental (custom built) |
+| Agent-fills workflow | Yes, via templates | Yes, via templates | Yes | Yes — explicit two-phase prep + sign flow |
+| MLS listing integration | No | No | Partial | WFRMLS/utahrealestate.com (full site integration) |
+| Field detection | Template-based | Template-based | Template-based | Heuristic auto-detect on Utah standard forms (v1.x) |
+
+## Sources
+
+**Real estate agent website features and best practices:**
+- [10 Must-Have Features for Real Estate Agent Websites — EuroDNS](https://www.eurodns.com/blog/10-must-have-features-for-high-performing-real-estate-agent-websites)
+- [15 Real Estate Web Design Features That Actually Drive Sales In 2025 — AltaStreet](https://www.altastreet.com/15-real-estate-web-design-features-that-actually-drive-sales-in-2025/)
+- [Real Estate Agent Website Design Trends 2025 — Fix8Media](https://www.fix8media.com/real-estate-agent-website-design-trends-2025)
+- [Real Estate Website Design Best Practices — AgentFire](https://agentfire.com/blog/real-estate-website-design-best-practices/)
+- [20 Real Estate Agent Bio Examples and Tips — InboundREM](https://inboundrem.com/real-estate-bio/)
+- [Real Estate Bio Examples: Copy-Paste Templates 2026 — Propphy](https://www.propphy.com/blog/real-estate-bio-examples-templates-2025)
+- [How to Create Effective Real Estate Testimonials — Carrot](https://carrot.com/blog/effective-real-estate-testimonial/)
+- [15 Real Estate Landing Page Best Practices — Landingi](https://landingi.com/landing-page/real-estate-best-practices/)
+
+**WFRMLS / IDX integration:**
+- [IDX Broker for Wasatch Front Regional MLS (WFRMLS)](https://www.idxbroker.com/mls/wasatch-front-regional-mls-wfrmls)
+- [Add Wasatch Front Regional MLS to Your Site — Showcase IDX](https://showcaseidx.com/mls-coverage/wasatch-front-regional-mls-wfrmls/)
+- [How to Connect WordPress to WFRMLS — Realtyna](https://realtyna.com/blog/how-to-connect-wordpress-website-to-wasatch-front-regional-mls-utah-wfrmls-with-organic-idx-mls-integration/)
+- [IDX MLS Listings for WFRMLS of Utah — ProAgent Websites](https://www.proagentwebsites.com/wfr.html)
+
+**Real estate document signing workflows:**
+- [eSignature Document Workflow Software for Real Estate — SigniFlow](https://www.signiflow.com/esignature-document-workflow-software-for-the-real-estate-sector-signiflow/)
+- [DocuSign for Real Estate & PandaDoc — PandaDoc](https://www.pandadoc.com/blog/docusign-real-estate-pandadoc-realtors/)
+- [5 Real Estate Automation Tools Every Agent Needs in 2025 — RealOffice360](https://realoffice360.com/crm-blog-for-realtors/real-estate-workflow-automation-tools-2025)
+- [DigiSign by SkySlope — E-Signature for Realtors](https://skyslope.com/products-services/digisign/)
+- [We Tested 15 Electronic Signature Tools That Streamline Real Estate Workflows — SignWell](https://www.signwell.com/resources/electronic-signature-for-real-estate/)
+- [E-Sign Real Estate Contracts: The Agent's Guide — Market Leader](https://www.marketleader.com/blog/esign-real-estate-contracts/)
+- [7 Document Collection Automation Tips for Real Estate Agents — UseCollect](https://www.usecollect.com/blog/7-document-collection-automation-tips-for-real-estate-agents/)
+- [Lone Wolf Authentisign — Real estate's leading eSignature solution](https://www.lwolf.com/operate/esignature)
+- [Electronic Signatures for Real Estate: 2026 Guide — DocuPilot](https://www.docupilot.com/blog/electronic-signature-for-real-estate)
+- [Redefining Real Estate: The Document Automation Transformation — Experlogix](https://www.experlogix.com/blog/redefining-real-estate-the-document-automation-transformation)
+
+**E-signature UX and PDF form field management:**
+- [Embedded Signing Experience Best Practices — eSignGlobal](https://www.esignglobal.com/blog/best-practices-embedded-signing-user-experience-ux)
+- [eSignature UX: Digital Excellence — Emudhra](https://emudhra.com/en-us/blog/user-experience-in-esignatures-designing-for-digital-excellence)
+- [Top eSignature Trends to Watch in 2026 — BlueInk](https://www.blueink.com/blog/top-esignature-trends-2026)
+- [Best Practices for Building Your E-Signature Workflow — OneSpan](https://www.onespan.com/blog/best-practices-building-your-e-signature-workflow)
+- [Top 13 E-Signature Mistakes Businesses Must Avoid — WeSignature](https://wesignature.com/blog/top-13-e-signature-mistakes-businesses-make/)
+- [Best Practices for eSignatures: Legally Binding and Secure — Acronis](https://www.acronis.com/en/blog/posts/best-practices-for-e-signature/)
+- [E-Signature Software Development In 8 Easy Steps — USM Systems](https://usmsystems.com/e-signature-software-development-in-8-easy-steps/)
+- [An Overview of DocHub's PDF Editing, Annotation and Signing Tools — DocHub](https://helpdesk.dochub.com/hc/en-us/articles/360019037293-An-overview-of-DocHub-s-PDF-editing-annotation-signing-tools)
+- [PDF Form Fields Supported in Document Engine — Nutrient](https://www.nutrient.io/guides/document-engine/forms/introduction-to-forms/form-fields/)
+- [US Electronic Signature Laws (ESIGN/UETA) — DocuSign](https://www.docusign.com/products/electronic-signature/learn/esign-act-ueta)
+
+**Utah-specific:**
+- [Utah Division of Real Estate — State Approved Forms](https://realestate.utah.gov/real-estate/forms/state-approved/)
+- [Utah Association of REALTORS Forms](https://utahrealtors.com/)
+
+---
+*Feature research for: Teressa Copeland Homes — real estate marketing site + document signing portal*
+*Researched: 2026-03-19*
diff --git a/.planning/research/PITFALLS.md b/.planning/research/PITFALLS.md
new file mode 100644
index 0000000..386f9c3
--- /dev/null
+++ b/.planning/research/PITFALLS.md
@@ -0,0 +1,510 @@
+# Pitfalls Research
+
+**Domain:** Real estate broker web app with custom e-signature and document signing (Utah/WFRMLS)
+**Researched:** 2026-03-19
+**Confidence:** HIGH
+
+---
+
+## Critical Pitfalls
+
+### Pitfall 1: Custom E-Signature Has No Tamper-Evident PDF Hash
+
+**What goes wrong:**
+The signed PDF is stored, but there is no cryptographic hash (SHA-256 or similar) computed at the moment of signing and embedded in or stored alongside the document. If the PDF is ever challenged in court, there is no way to prove the document was not modified after signing. The signature image becomes legally just "a drawing on a page."
+
+**Why it happens:**
+Developers focus on capturing the signature canvas image and embedding it into the PDF. The hash/integrity step feels like an edge case until a dispute occurs years later. Utah courts and ESIGN/UETA require the system to prove document integrity, not just that a signature image exists.
+
+**How to avoid:**
+After embedding the signature image into the PDF and before storing the final file: compute a SHA-256 hash of the complete signed PDF bytes. Store this hash in the database record alongside the document. Optionally, embed the hash and a certificate-of-completion JSON blob as a PDF metadata/attachment. On any challenge, recompute the hash against the stored file and compare.
+
+**Warning signs:**
+- The signing record only stores the signature image URL and a timestamp, with no document fingerprint.
+- No "certificate of completion" is generated alongside the signed PDF.
+- The audit log references document events but not the final document hash.
+
+**Phase to address:**
+Document signing backend — before any client is sent a signing link.
+
+---
+
+### Pitfall 2: Audit Trail is Incomplete and Would Fail Court Challenge
+
+**What goes wrong:**
+The system logs that a document was signed and stores an IP address. But if challenged, the opposing party argues the signer's identity cannot be verified, the viewing timestamp is missing, or the sequence of events (sent → opened → signed) cannot be reconstructed. Courts have rejected e-signatures precisely because the audit trail only showed the signature event, not the full ceremony.
+
+**Why it happens:**
+Developers build the "happy path" (sign button clicked → PDF stored). Pre-signing events (email sent, link opened, document viewed) are not logged because they seem unimportant until litigation. Federal district courts have held that detailed e-signature audit logs satisfy authentication requirements; gaps in the log create exploitable weaknesses.
+
+**How to avoid:**
+Log every event in the signing ceremony as a separate, timestamped, server-side record: (1) document prepared, (2) signing email sent with link hash, (3) link opened (IP, user agent, timestamp), (4) document viewed/scrolled, (5) signature canvas drawn/submitted, (6) final PDF hash computed and stored. All events must be stored server-side — never trust client-reported timestamps. Include: signer email, IP address, user-agent string, timezone offset, and event type for every record.
+
+**Warning signs:**
+- Audit records are only written on the POST to submit the signature, not on GET of the signing page.
+- Timestamps come from the client's browser clock.
+- "Email sent" is logged in the email provider's dashboard but not in the app's own database.
+
+**Phase to address:**
+Email-link signing flow implementation; auditing must be wired in before first signing ceremony, not added later.
+
+---
+
+### Pitfall 3: Signing Link is Replayable and Has No One-Time Enforcement
+
+**What goes wrong:**
+A signing link is emailed to the client. The link contains a token (e.g., a UUID or JWT). After the client signs, the link still works — anyone who intercepts, forwards, or finds the link in an inbox can re-open the signing page, potentially submitting a second signature or viewing the partially signed document.
+
+**Why it happens:**
+Developers treat the signing token as a read-only session key. Invalidating it requires server-side state, which feels at odds with stateless JWT approaches. The edge case of link forwarding or second-device access feels rare.
+
+**How to avoid:**
+Signing tokens must be stored in the database with a `used_at` timestamp column. On every request to the signing page, check: (1) token exists, (2) token is not expired (recommend 72-hour TTL for real estate — clients may not check email immediately, but 15-minute windows used in authentication magic links are too short for document signing), (3) `used_at` is null. On signature submission success, set `used_at` immediately before returning the success response. Use a database transaction to prevent race conditions. If the link is accessed after use, display "This document has already been signed" with a link to contact the agent — never re-render the signing canvas.
+
+**Warning signs:**
+- Signing token is a stateless JWT with no server-side revocation mechanism.
+- Loading the signing URL twice shows the signature canvas both times.
+- No `signed_at` or `used_at` column exists on the signing link record.
+
+**Phase to address:**
+Email-link signing flow — token generation and validation logic must include one-time enforcement from the start.
+
+---
+
+### Pitfall 4: PDF Coordinate System Mismatch Places Signatures in Wrong Positions
+
+**What goes wrong:**
+Signature fields are placed on the PDF using coordinates from the browser's click/drag UI (origin: top-left, Y increases downward). When the signature image is embedded into the PDF using pdf-lib or a similar library (origin: bottom-left, Y increases upward), the signature appears in the wrong position — often mirrored vertically or offset significantly. This gets worse with rotated PDF pages (common in scanned real estate forms).
+
+**Why it happens:**
+PDF's coordinate system is inherited from PostScript and is the opposite of every web/canvas coordinate system. Developers test with a simple unrotated letter-size PDF and it appears to work, then discover the mismatch when processing actual Utah real estate forms that may be rotated or have non-standard page sizes.
+
+**How to avoid:**
+Always convert click/placement coordinates from viewer space to PDF space using the library's own conversion APIs. In PDF.js: use `viewport.convertToPdfPoint(x, y)`. In pdf-lib: the page's height must be subtracted from Y coordinates when mapping from top-left screen space. Write a coordinate conversion unit test against a known PDF page size: place a signature at the visual center of the page and assert the embedded result matches `(pageWidth/2, pageHeight/2)` in PDF points. For rotated pages, read and apply the page's `Rotate` key before computing placement — a page rotated 90 degrees requires a full matrix transform, not just a Y-flip.
+
+**Warning signs:**
+- Signature placement is tested only on a single blank PDF created in the app, not on actual Utah real estate forms.
+- The placement code contains a raw Y subtraction without checking for page rotation.
+- Signatures look correct in the browser preview but misaligned in the downloaded PDF.
+
+**Phase to address:**
+PDF field detection and signature placement — before any user-facing drag-and-drop placement UI is built.
+
+---
+
+### Pitfall 5: Scraping utahrealestate.com Forms Library Violates Terms of Service
+
+**What goes wrong:**
+The app authenticates to utahrealestate.com with Teressa's credentials and scrapes the forms library (PDFs of purchase agreements, listing agreements, etc.) using HTTP requests or a headless browser. This appears to work initially, then breaks when the site updates its session handling, adds bot detection, or changes URL structures. More critically, it violates the platform's Terms of Service and the WFRMLS data licensing agreement, which restricts data use to authorized products only.
+
+**Why it happens:**
+utahrealestate.com has partnered with SkySlope Forms for its digital forms solution. There is no documented public API for the forms library. Using Teressa's credentials to download PDFs looks like a clean solution — it's just automating what she does manually. The ToS implication is easy to overlook.
+
+**How to avoid:**
+Do not scrape the utahrealestate.com forms library programmatically. Instead: (1) Identify which forms Teressa uses most frequently (purchase agreement, listing agreement, buyer rep agreement, addendums). (2) Download those PDFs manually once and store them as static assets in the app's own file storage. Utah Division of Real Estate also publishes state-approved forms publicly at commerce.utah.gov — these are explicitly public domain and safe to embed. (3) For any forms that must come from the MLS, treat the upload step as a manual agent action: Teressa uploads the PDF herself from her downloads, then the app processes it. This also means the app is not fragile to site changes.
+
+**Warning signs:**
+- The app stores Teressa's utahrealestate.com password in its own database or environment variables to automate login.
+- The forms-fetching code contains hardcoded URLs pointing to utahrealestate.com paths.
+- A site redesign or session expiry would break the core import flow.
+
+**Phase to address:**
+Forms import feature — the architecture decision must be made before any scraping code is written.
+
+---
+
+### Pitfall 6: IDX Listings Displayed Without Required Broker Attribution and Disclaimers
+
+**What goes wrong:**
+Listings from WFRMLS are displayed on the public site without the required listing broker attribution (the co-listing brokerage name, not just Teressa's), without the MLS-required disclaimer text, or with data that has been modified (e.g., description truncated, price formatted differently). NAR IDX policy violations can result in fines up to $15,000 or loss of MLS access — which would effectively end Teressa's ability to operate.
+
+**Why it happens:**
+IDX compliance rules feel like fine print. Developers display the data that looks good and skip the attribution fields because they seem redundant on a solo agent site. The disclaimer is added as a one-time footer and then forgotten when listing detail pages are added later.
+
+**How to avoid:**
+Every listing display page (card and detail) must render: (1) listing broker/office name from the `ListOfficeName` field, not Teressa's brokerage; (2) the WFRMLS-required disclaimer text verbatim on every page where IDX data appears; (3) a "Last Updated" timestamp pulled from the feed, not the app's cache write time; (4) no modified listing descriptions or prices. Implement a compliance checklist as a code review gate before any listing-display page ships. Confirm the exact required disclaimer text with WFRMLS directly — it changes with NAR policy updates (2024 settlement, 2025 seller options policy).
+
+**Warning signs:**
+- Listing detail pages render only the property data, with no co-brokerage attribution field.
+- The IDX disclaimer appears only on the search results page, not on individual listing detail pages.
+- The app omits the buyer's agent compensation disclosure field added as a NAR 2024 settlement requirement.
+
+**Phase to address:**
+Public listings feature — compliance requirements must be treated as acceptance criteria, not post-launch polish.
+
+---
+
+### Pitfall 7: Stale Listings Show Off-Market Properties as Active
+
+**What goes wrong:**
+The app caches WFRMLS listing data in a database and refreshes it on a slow schedule (e.g., once per day via a cron job, or only when a user loads the page). A property that went under contract or sold yesterday still shows as active on the site. Clients contact Teressa about a property that is no longer available, damaging her professional credibility. WFRMLS policy expects off-market listings to be removed within 24 hours.
+
+**Why it happens:**
+Frequent API polling adds infrastructure complexity. A daily batch job feels sufficient. The RESO OData API returns a `ModificationTimestamp` field that allows delta syncing, but developers often do full re-fetches instead, which is slow and rate-limited.
+
+**How to avoid:**
+Implement delta sync: query the WFRMLS RESO API with a `$filter` on `ModificationTimestamp gt [last_sync_time]` to fetch only changed listings since the last run. Schedule this to run hourly via a reliable background job (not WP-Cron or in-process timers — use a proper scheduler like Vercel Cron or a dedicated worker). When a listing's `StandardStatus` transitions to Closed, Withdrawn, or Expired, remove it from the public display immediately, not at next full refresh. Display a "Last Updated" timestamp on each listing page so both users and MLS auditors can verify freshness.
+
+**Warning signs:**
+- The sync job runs once per day.
+- The sync is triggered by a user page load rather than a scheduled job.
+- There is no `ModificationTimestamp` filter in the API query — all listings are re-fetched every run.
+- The app has no process for immediately hiding a listing when its status changes to off-market.
+
+**Phase to address:**
+Listings sync infrastructure — before the public site goes live with real listing data.
+
+---
+
+### Pitfall 8: PDF Form Field Detection Relies on Heuristics That Fail on Non-Standard Forms
+
+**What goes wrong:**
+The app attempts to auto-detect signature fields in uploaded Utah real estate PDFs using text-layer heuristics (searching for the word "Signature" or "_____" underscores). This works on forms with searchable text layers, fails silently on scanned PDFs (no text layer), and places fields incorrectly on multi-column forms where the heuristic matches the wrong block.
+
+**Why it happens:**
+Auto-detection feels like the right UX goal — the agent should not have to manually place every field. PDF form field detection is non-trivial: some forms have AcroForm fields, some have visual annotation markers only, and some are flat scans. pdf-lib does not expose field dimensions/coordinates natively (a known open issue since 2020), so custom heuristics are required.
+
+**How to avoid:**
+Treat auto-detection as a "best effort starting point," not a reliable system. The flow should be: (1) attempt AcroForm field parsing first — if the PDF has embedded AcroForm fields, use them; (2) if not, present a manual drag-to-place UI where Teressa visually positions signature and date fields on a PDF.js-rendered preview; (3) never send a document to a client without Teressa having confirmed field placements. This manual confirmation step is also a legal safeguard — it proves the agent reviewed the document before sending. Store field placements in PDF coordinates (bottom-left origin, points) in the database, not screen coordinates.
+
+**Warning signs:**
+- The heuristic search for signature fields is the only detection method, with no fallback UI.
+- Field placements are stored in screen pixels without converting to PDF coordinate space.
+- The app has never been tested with a scanned (non-OCR'd) PDF.
+
+**Phase to address:**
+PDF field detection and form preparation UI — design the manual fallback first, then add auto-detection as an enhancement.
+
+---
+
+### Pitfall 9: Font Not Embedded in PDF Before Flattening Causes Text to Disappear
+
+**What goes wrong:**
+Teressa fills in text fields (property address, client name, price) on the PDF. When the form is flattened (fields baked into the page content stream), the text either disappears, renders as boxes, or uses a substitute font that looks wrong. This happens when the font referenced by the AcroForm field is not embedded in the PDF file and is not available in the server-side processing environment.
+
+**Why it happens:**
+AcroForm fields reference fonts by name (e.g., "Helvetica"). On a desktop PDF viewer, these fonts are available system-wide and render correctly. On a headless server-side Node.js environment (e.g., Vercel serverless function), no system fonts are installed. When the field is flattened, the text cannot be rendered because the font is absent.
+
+**How to avoid:**
+Before flattening any filled form, ensure all fonts referenced by form fields are embedded in the PDF. With pdf-lib, embed the font explicitly: load a standard font (e.g., StandardFonts.Helvetica from pdf-lib's built-in set) and set it on each form field before calling `form.flatten()`. For forms that use custom fonts, embed the TTF/OTF font file directly. Test flattening in the exact serverless environment where it will run (not locally on a Mac with system fonts installed).
+
+**Warning signs:**
+- PDF flattening is tested only on a local development machine.
+- The serverless function environment has no system fonts installed and this has not been verified.
+- Filled form text is visible in the browser PDF preview (which uses browser fonts) but blank in the downloaded flattened PDF.
+
+**Phase to address:**
+PDF form filling and flattening — must be validated in the production environment, not just locally.
+
+---
+
+### Pitfall 10: Mobile Signing UX Makes Canvas Signature Unusable
+
+**What goes wrong:**
+Clients open the signing link on a mobile phone (the majority of email opens are on mobile). The signature canvas scrolls the page instead of capturing the drawing gesture. The canvas is too small to draw a legible signature. The "Submit" button is off-screen. The client gives up and calls Teressa to ask how to sign, creating friction that defeats the purpose of the email-link flow.
+
+**Why it happens:**
+The signing page is designed and tested on desktop. The signature canvas uses a standard `