docs: add domain research
This commit is contained in:
703
.planning/research/ARCHITECTURE.md
Normal file
703
.planning/research/ARCHITECTURE.md
Normal file
@@ -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 <AgentShell user={session.user}>{children}</AgentShell>;
|
||||
}
|
||||
```
|
||||
|
||||
### 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<string> {
|
||||
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*
|
||||
289
.planning/research/FEATURES.md
Normal file
289
.planning/research/FEATURES.md
Normal file
@@ -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*
|
||||
510
.planning/research/PITFALLS.md
Normal file
510
.planning/research/PITFALLS.md
Normal file
@@ -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 `<canvas>` element with touch event listeners added, but the browser's default touch behavior (page scroll) intercepts the gesture before the canvas can capture it. `touch-action: none` is not applied.
|
||||
|
||||
**How to avoid:**
|
||||
Apply `touch-action: none` on the signature canvas element to prevent the browser from intercepting touch gestures. Set the canvas to fill the full viewport width on mobile (100vw minus padding). Implement pinch-zoom prevention on the signing page only (meta viewport `user-scalable=no`) so accidental zoom does not distort the canvas. Test the complete signing flow on iOS Safari and Android Chrome — these are the two browsers with the most idiosyncratic touch handling. Consider using the `signature_pad` npm library (szimek/signature_pad) which handles touch normalization across devices. Display a "Draw your signature here" placeholder inside the canvas that disappears on first touch.
|
||||
|
||||
**Warning signs:**
|
||||
- The signing page has only been tested on desktop Chrome.
|
||||
- The canvas element has no `touch-action` CSS property set.
|
||||
- A physical phone test shows the page scrolling instead of capturing strokes.
|
||||
|
||||
**Phase to address:**
|
||||
Client-facing signing UI — mobile must be a primary test target from the first implementation, not a responsive afterthought.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 11: iOS Safari Canvas Self-Clearing and Vertical-Line Bug
|
||||
|
||||
**What goes wrong:**
|
||||
On iOS 15+, the signature canvas occasionally wipes itself mid-draw. On iOS 13, drawing vertical lines registers as a single point rather than a stroke — so signatures with vertical elements look like dots. These bugs are silent: the app appears to work, but the exported signature is illegible or empty, and that illegible image gets embedded in a legal document.
|
||||
|
||||
**Why it happens:**
|
||||
iOS 15 introduced a canvas self-clearing bug triggered by the Safari URL bar resizing the viewport while the user is drawing. The iOS 13 vertical stroke bug is a platform-level touch event regression. These do not reproduce in Chrome DevTools mobile emulation or on the iOS simulator — they require physical devices to discover.
|
||||
|
||||
**How to avoid:**
|
||||
- Use `signature_pad` version >= 1.0.6 (szimek/signature_pad) or `react-signature-canvas` >= 1.0.6 which includes the iOS 15 workaround
|
||||
- Set `preserveDrawingBuffer: true` on the canvas context when creating it to survive viewport resize events
|
||||
- Before accepting the submitted signature, validate that the canvas contains a meaningful number of non-background pixels — reject and prompt re-draw if the image is effectively blank
|
||||
- Export the canvas at 2x resolution (set `canvas.width` and `canvas.height` to 2x the CSS display size, then scale the 2D context by 2) so Retina displays produce a sharp embedded image
|
||||
- Test signing on physical iOS 13, iOS 15, and latest iOS devices — not in simulator
|
||||
|
||||
**Warning signs:**
|
||||
- Signed PDFs coming back from real clients have blank or near-blank signature images
|
||||
- The signature canvas library has not been updated in over a year
|
||||
- `preserveDrawingBuffer` is not explicitly set in the canvas initialization
|
||||
|
||||
**Phase to address:**
|
||||
Client-facing signing UI — physical device testing required before any real client document is sent.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 12: Signed PDFs Are Accessible via Guessable URLs (IDOR)
|
||||
|
||||
**What goes wrong:**
|
||||
An authenticated user (any agent or even a client who has signed their own document) modifies the document ID in the download URL — `/api/documents/1234/download` becomes `/api/documents/1233/download` — and successfully downloads another client's signed document. This is an Insecure Direct Object Reference (IDOR), consistently in the OWASP Top 10, and is a serious privacy violation in a real estate context where documents contain personal financial information.
|
||||
|
||||
**Why it happens:**
|
||||
The download route checks that the user is authenticated (has a valid session cookie) but does not verify that the authenticated user owns the requested document. Sequential integer IDs make enumeration trivial. This pattern is easy to miss because it requires testing with two separate accounts — single-account testing never reveals it.
|
||||
|
||||
**How to avoid:**
|
||||
- Use UUID v4 (not sequential integers) for all document IDs in URLs and API routes
|
||||
- In every API route handler that returns a document or its metadata, query the database to confirm `document.agent_id === session.agent_id` (or the equivalent ownership check) before streaming or returning the file
|
||||
- Do not rely solely on Next.js middleware for this check — middleware can be bypassed via CVE-2025-29927 (see Pitfall 13); put the ownership check inside the route handler itself
|
||||
- Store signed PDFs in a private object storage bucket (S3, Supabase Storage with RLS, Cloudflare R2) and generate short-lived pre-signed URLs (15 minutes or less) for downloads — never serve from a static public URL
|
||||
- Log every document download with the authenticated user's ID and timestamp for audit purposes
|
||||
|
||||
**Warning signs:**
|
||||
- Document download URLs contain sequential integers (e.g., `/documents/47`)
|
||||
- The download route handler does not perform a database ownership query before responding
|
||||
- PDFs are served from a publicly accessible storage bucket URL
|
||||
|
||||
**Phase to address:**
|
||||
Document storage and download API — UUID identifiers and ownership checks must be in place before any real client documents are uploaded.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 13: Next.js Middleware as the Only Authorization Gate (CVE-2025-29927)
|
||||
|
||||
**What goes wrong:**
|
||||
Authorization for sensitive routes (admin portal, document downloads, signing status) is implemented exclusively in Next.js middleware. An attacker adds the `x-middleware-subrequest` header to their HTTP request and bypasses middleware entirely, gaining unauthenticated access to protected routes. This is CVE-2025-29927, disclosed in March 2025, affecting all Next.js versions before 14.2.25 and 15.2.3.
|
||||
|
||||
**Why it happens:**
|
||||
Next.js middleware feels like the natural place to handle auth redirects. Developers add `middleware.ts` that checks for a session cookie and redirects unauthenticated users. This pattern is documented in the Next.js docs and is widely used — but the vulnerability demonstrated that middleware-only protection is not sufficient.
|
||||
|
||||
**How to avoid:**
|
||||
- Update Next.js to >= 14.2.25 or >= 15.2.3 immediately
|
||||
- Treat middleware as a first-layer UX redirect, not as a security enforcement layer
|
||||
- Add an explicit session/authorization check inside every API route handler and every server component that renders sensitive data — do not assume middleware has already validated the request
|
||||
- If deployed behind a reverse proxy (Nginx, Cloudflare, AWS ALB), configure it to strip the `x-middleware-subrequest` header from incoming requests
|
||||
- The rule: "Middleware redirects unauthenticated users; route handlers enforce authorization" — both layers must exist
|
||||
|
||||
**Warning signs:**
|
||||
- The `middleware.ts` file is the only place where session validation appears in the codebase
|
||||
- API routes do not individually check `getSession()` or `getToken()` before returning data
|
||||
- The Next.js version is older than 14.2.25
|
||||
|
||||
**Phase to address:**
|
||||
API route implementation — every route handler that touches client or document data must include an in-handler auth check regardless of middleware state.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 14: Credential-Based utahrealestate.com Scraping Has CFAA Exposure
|
||||
|
||||
**What goes wrong:**
|
||||
Using Teressa's utahrealestate.com credentials to automate form downloads is not just a Terms of Service question — it has Computer Fraud and Abuse Act (CFAA) exposure. A 2024 federal jury verdict (Ryanair vs. Booking.com, Delaware) found a scraping company violated the CFAA and established intent to defraud by accessing data behind a login wall, even using a third-party's credentials. The court further found that using techniques to avoid detection (rotating sessions, changing user-agent strings) separately supported the "intent to defraud" element.
|
||||
|
||||
**Why it happens:**
|
||||
Developers treat "using your own credentials" as inherently authorized. But CFAA analysis asks whether the automated programmatic use exceeds the authorized use defined in the ToS, not just whether credentials are valid. The 2022 hiQ v. LinkedIn ruling protecting public data scraping explicitly does not extend to data behind authentication barriers.
|
||||
|
||||
**How to avoid:**
|
||||
- Do not write automated credential-based scraping code for utahrealestate.com under any circumstances without a written API or data agreement from the platform
|
||||
- The correct architecture: Teressa manually downloads needed form PDFs from utahrealestate.com and uploads them into the app's admin portal. This is a one-time-per-form-update action, not per-client.
|
||||
- For state-approved real estate forms, the Utah Division of Real Estate publishes them at commerce.utah.gov/realestate — these are public domain and safe to bundle directly in the app
|
||||
- If programmatic access is genuinely needed, contact utahrealestate.com to request a formal data agreement or API key before writing any code
|
||||
|
||||
**Warning signs:**
|
||||
- Any code in the repository that performs an automated login to utahrealestate.com
|
||||
- Teressa's MLS password is stored anywhere in the application's configuration or database
|
||||
- The app has hardcoded selectors or URLs pointing to utahrealestate.com form library paths
|
||||
|
||||
**Phase to address:**
|
||||
Forms import architecture — this decision must be made and documented before any integration code is written.
|
||||
|
||||
---
|
||||
|
||||
## Technical Debt Patterns
|
||||
|
||||
Shortcuts that seem reasonable but create long-term problems.
|
||||
|
||||
| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|
||||
|----------|-------------------|----------------|-----------------|
|
||||
| Storing Teressa's utahrealestate.com credentials in `.env` for scraping | Avoids building a manual upload flow | ToS violation; breaks when site changes; security liability if env is leaked | Never — use manual PDF upload instead |
|
||||
| Using client-side timestamp for signing event | Simpler code, no server roundtrip | Timestamps are falsifiable; legally worthless as audit evidence | Never for legal audit trail |
|
||||
| Skipping PDF coordinate conversion unit tests | Faster initial development | Silent placement bugs on real-world forms; discovered only when a client signs in the wrong place | Never — write the test before building the UI |
|
||||
| Stateless JWT as signing token with no DB revocation | Simpler auth, no DB lookup on link access | Tokens cannot be invalidated; replay attacks are undetectable | Never for one-time signing links |
|
||||
| Hardcoding IDX disclaimer text in a component | Quick to ship | Disclaimer text changes with NAR policy; requires code deploy to update | Acceptable only if a content management hook is added in the next phase |
|
||||
| Full listing re-fetch instead of delta sync | Simpler initial implementation | Rate limit exhaustion; slow sync; stale off-market listings | Acceptable in Phase 1 dev/test only, must be replaced before public launch |
|
||||
| Skipping form flattening (storing unflattenend PDFs with live AcroForm fields) | Faster PDF processing | Signed documents can be edited after the fact in any PDF editor; legally indefensible | Never for final signed documents |
|
||||
| Using sequential integer IDs for document URLs | Simpler schema | IDOR vulnerability — any user can enumerate other clients' documents by changing the number in the URL | Never — use UUID v4 |
|
||||
| Putting all auth logic in Next.js middleware only | Less code per route | CVE-2025-29927 bypass allows unauthenticated access by adding a single HTTP header | Never — always add in-handler auth checks |
|
||||
| Skipping physical iOS device testing for signature canvas | Faster QA iteration | iOS 13 vertical stroke bug and iOS 15 canvas self-clearing are invisible in simulator | Never for any client-facing signing release |
|
||||
|
||||
---
|
||||
|
||||
## Integration Gotchas
|
||||
|
||||
Common mistakes when connecting to external services.
|
||||
|
||||
| Integration | Common Mistake | Correct Approach |
|
||||
|-------------|----------------|------------------|
|
||||
| WFRMLS RESO API | Using `$filter` with wrong field name casing (e.g., `modificationtimestamp` instead of `ModificationTimestamp`) — all field names are case-sensitive | Always copy field names exactly from the RESO metadata endpoint response; do not guess or lowercase |
|
||||
| WFRMLS RESO API | Expecting sold/closed price data in IDX feed | Utah is a non-disclosure state; closed price data is not available in IDX feeds; design UI without sale price history |
|
||||
| WFRMLS RESO API | Assuming instant vendor credential provisioning | Vendor approval requires contract signing, background check, and compliance review; allow 2–4 weeks lead time |
|
||||
| utahrealestate.com forms | Automating PDF downloads with Teressa's session cookies | Terms of Service prohibit automated data extraction; use manual upload or state-published public forms |
|
||||
| Email delivery (signing links) | Relying on `localhost` or unverified sender domain for transactional email | Signing emails go to spam; SPF/DKIM/DMARC must be configured on teressacopelandhomes.com before first client send |
|
||||
| pdf-lib form flattening | Calling `form.flatten()` without first embedding fonts | Text fields render blank or with substitute fonts on server-side; embed fonts explicitly before flattening |
|
||||
| pdf-lib signature image placement | Using screen/canvas Y coordinate directly as the pdf-lib Y value | PDF origin is bottom-left; screen origin is top-left — always apply `pdfY = page.getHeight() - (canvasY / viewportScale)` |
|
||||
| Document download API | Checking authentication only ("is logged in?") rather than authorization ("does this user own this document?") | Perform an ownership database query inside the route handler; use UUID document IDs; serve from private storage with short-lived signed URLs |
|
||||
|
||||
---
|
||||
|
||||
## Performance Traps
|
||||
|
||||
Patterns that work at small scale but fail as usage grows.
|
||||
|
||||
| Trap | Symptoms | Prevention | When It Breaks |
|
||||
|------|----------|------------|----------------|
|
||||
| Full WFRMLS listing re-fetch on every sync | Slow sync jobs; API rate limit errors; listings temporarily unavailable during fetch | Implement delta sync using `ModificationTimestamp` filter | At ~100+ listings or hourly sync schedule |
|
||||
| Generating signed PDFs synchronously in an API route | Signing page times out; Vercel/Next.js 10s function timeout exceeded | Move PDF generation to a background job or streaming response | On PDFs larger than ~2MB or with complex form filling |
|
||||
| Storing signed PDFs in the Next.js app's local filesystem | PDFs lost on serverless deployment; no persistence across function instances | Store all documents in S3-compatible object storage (e.g., AWS S3, Cloudflare R2) from day one | On first serverless deployment |
|
||||
| Loading full-page PDF.js bundle on every page of the site | Slow initial page load on the public marketing site | Code-split the PDF viewer — load it only on the agent dashboard and signing page routes | Immediately — affects all visitors |
|
||||
|
||||
---
|
||||
|
||||
## Security Mistakes
|
||||
|
||||
Domain-specific security issues beyond general web security.
|
||||
|
||||
| Mistake | Risk | Prevention |
|
||||
|---------|------|------------|
|
||||
| Signing link token is not hashed at rest in the database | Database breach exposes all active signing tokens; attacker can sign documents on behalf of clients | Store HMAC-SHA256(token) in DB; compare hash on request, never the raw token |
|
||||
| No rate limit on signing link generation | Attacker floods Teressa's client email inboxes with signing emails from the app | Rate limit signing link creation per document and per agent session (max 3 resends per document) |
|
||||
| Signed PDFs accessible via guessable URL (e.g., `/documents/123`) | Sequential ID enumeration lets anyone download any signed document | Use unguessable UUIDs for document storage paths and signed URLs with short TTL for downloads |
|
||||
| Agent session not invalidated on agent logout | If Teressa leaves a browser tab open on a shared computer, her entire client document library is accessible | Implement server-side session invalidation on logout; do not rely on client-side cookie deletion alone |
|
||||
| Signature canvas accepts 0-stroke "signatures" | Client submits a blank canvas as their signature; the signed document has no visible signature mark | Validate that the canvas has at least a minimum number of non-white pixels / stroke events before accepting submission |
|
||||
| No CSRF protection on the signature submission endpoint | Cross-site request forgery could submit a forged signature | The signing token in the URL/body acts as an implicit CSRF token only if validated server-side on every request; add `SameSite=Strict` to any session cookies |
|
||||
| Signed PDFs at predictable/guessable URLs (sequential integer IDs) | Any authenticated user can enumerate and download all signed documents; real estate documents contain SSNs, financial info, home addresses | Use UUID v4 for document IDs; store in private bucket; issue short-lived pre-signed download URLs per-request |
|
||||
| Next.js middleware as sole authorization layer | CVE-2025-29927 allows middleware bypass with a crafted HTTP header; attacker accesses any protected route without authentication | Upgrade Next.js to >= 14.2.25; add ownership check in every API route handler independent of middleware |
|
||||
| Blank canvas accepted as a valid signature | Empty or near-empty PNG gets embedded in the signed document as the client's legally binding signature | Server-side validation: reject submissions where the canvas image has fewer than a minimum number of non-background pixels or fewer than a minimum number of recorded stroke events |
|
||||
|
||||
---
|
||||
|
||||
## UX Pitfalls
|
||||
|
||||
Common user experience mistakes in this domain.
|
||||
|
||||
| Pitfall | User Impact | Better Approach |
|
||||
|---------|-------------|-----------------|
|
||||
| Showing "Invalid token" when a signing link is expired | Client calls Teressa confused; does not know what to do next | Show "This signing link has expired. Contact Teressa Copeland to request a new one." with her phone/email pre-filled |
|
||||
| Showing "Invalid token" when a signing link has already been used | Client thinks something is broken; may attempt to sign again | Show "You have already signed this document. Teressa has your signed copy." with a reassuring message |
|
||||
| Forcing client to scroll through the entire document before the "Sign" button appears | Clients on mobile give up before reaching the signature field | Provide a sticky "Jump to Signature" button; do not hide the sign action behind mandatory scrolling |
|
||||
| No confirmation screen after signing | Client does not know if the submission worked | Show a clear "Document Signed Successfully" confirmation with the document name and a timestamp; optionally send a confirmation email |
|
||||
| Agent has no way to see which documents are awaiting signature vs. completed | Teressa loses track of unsigned documents | Dashboard must show document status at a glance: Draft / Sent / Signed / Archived — never mix them in a flat list |
|
||||
| Client-facing signing page has Teressa's agent branding (logo, nav) mixed with client-action UI | Client is confused about whether they are on Teressa's site or a signing service | Keep the signing page minimal: document title, Teressa's name as sender, the document, the signature canvas, and a submit button — no site navigation |
|
||||
|
||||
---
|
||||
|
||||
## "Looks Done But Isn't" Checklist
|
||||
|
||||
Things that appear complete but are missing critical pieces.
|
||||
|
||||
- [ ] **E-signature capture:** Signing canvas works in Chrome desktop — verify it captures correctly on iOS Safari and Android Chrome with touch gestures (not scroll events).
|
||||
- [ ] **PDF signing:** Signature appears in the browser PDF preview — verify the position is correct in the actual downloaded PDF file opened in Adobe Reader or Preview.
|
||||
- [ ] **Audit trail:** Events are logged — verify the log includes: email sent, link opened, document viewed, signature submitted, final PDF hash, all with server-side timestamps.
|
||||
- [ ] **Signing link security:** Link opens the signing page — verify the link cannot be used a second time after signing and expires after 72 hours even if unused.
|
||||
- [ ] **Font flattening:** Form fields fill correctly locally — verify the filled and flattened PDF looks correct when generated in the production serverless environment (not just on a Mac with system fonts).
|
||||
- [ ] **IDX compliance:** Listings display on the public site — verify every listing card and detail page has: listing broker attribution, MLS disclaimer text, last updated timestamp, and buyer's agent compensation field per 2024 NAR settlement.
|
||||
- [ ] **Stale listings:** Sync job runs — verify the job uses `ModificationTimestamp` delta filtering and removes off-market listings within 1 hour of status change, not at next full refresh.
|
||||
- [ ] **PDF coordinate placement:** Signature field drag-and-drop works on a blank PDF — verify placement coordinates are correct on an actual Utah real estate purchase agreement form, including any rotated pages.
|
||||
- [ ] **Document storage:** PDFs save — verify signed PDFs are stored in persistent object storage (S3/R2), not the local filesystem, and that URLs survive a fresh deployment.
|
||||
- [ ] **Document IDOR:** Document download works for the document owner — verify with a second test account that changing the document ID in the URL returns a 403 or 404, not the other account's document.
|
||||
- [ ] **iOS canvas bugs:** Signing canvas works on Chrome desktop — verify on a physical iPhone (iOS 13 and iOS 15+) in Safari that vertical strokes register correctly and the canvas does not clear itself during drawing.
|
||||
- [ ] **Blank signature rejection:** Submitting a signature works — verify that clicking Submit on an untouched blank canvas returns a validation error rather than producing a signed document with an empty signature image.
|
||||
- [ ] **Next.js auth:** Route protection appears to work via middleware — verify that every sensitive API route handler contains its own session/ownership check independent of middleware by testing with a crafted request that bypasses middleware.
|
||||
- [ ] **CFAA compliance:** Forms import works — verify there is no code in the repository that performs automated login to utahrealestate.com; all forms must enter the system via manual agent upload.
|
||||
|
||||
---
|
||||
|
||||
## Recovery Strategies
|
||||
|
||||
When pitfalls occur despite prevention, how to recover.
|
||||
|
||||
| Pitfall | Recovery Cost | Recovery Steps |
|
||||
|---------|---------------|----------------|
|
||||
| Signed PDF lacks document hash; legal challenge | HIGH | Reconstruct signing ceremony evidence from audit log events, IP records, email delivery logs; engage legal counsel; this cannot be fully recovered — prevention is the only answer |
|
||||
| utahrealestate.com scraping blocked or ToS violation notice | MEDIUM | Immediately disable scraper; switch to manual PDF upload flow; apologize to MLS; a manually-uploaded forms library was the right design from the start |
|
||||
| IDX compliance violation discovered (missing attribution) | MEDIUM | Fix attribution immediately across all listing pages; contact WFRMLS compliance to self-report before they audit; document the fix with timestamps |
|
||||
| Signing link replay attack (duplicate signature submitted) | MEDIUM | Add `used_at` validation to token check; mark the earlier duplicate submission as invalid in the audit log; notify affected signer via email |
|
||||
| Signatures misplaced in PDFs (coordinate bug) | MEDIUM | Identify all affected documents; mark them as invalid in the system; Teressa must re-send for re-signing; fix coordinate conversion and add the regression test |
|
||||
| PDF font flattening failure (blank text in signed PDFs) | LOW-MEDIUM | Embed standard fonts explicitly in the flattening step; regenerate affected documents; re-send for signing if clients have not yet signed |
|
||||
| Stale listing shown to client who made an offer on unavailable property | LOW | Implement hourly delta sync immediately; add a "verify availability" disclaimer to listing pages in the short term |
|
||||
|
||||
---
|
||||
|
||||
## Pitfall-to-Phase Mapping
|
||||
|
||||
How roadmap phases should address these pitfalls.
|
||||
|
||||
| Pitfall | Prevention Phase | Verification |
|
||||
|---------|------------------|--------------|
|
||||
| No tamper-evident PDF hash | Document signing backend | Compute and store SHA-256 hash on every signed PDF; verify by recomputing and comparing |
|
||||
| Incomplete audit trail | Email-link signing flow | Trace a full test signing ceremony and confirm all 6 event types appear in the audit log |
|
||||
| Replayable signing link | Email-link signing flow | After signing, reload the signing URL and confirm it returns "already signed" — not the canvas |
|
||||
| PDF coordinate mismatch | PDF field placement UI | Place a test signature on a known position in an actual Utah purchase agreement; open in Acrobat and verify visual position |
|
||||
| utahrealestate.com forms scraping | Forms import architecture | No scraping code exists in the codebase; PDF source is manual upload or public state forms only |
|
||||
| IDX display without required attribution | Public listings feature | Audit every listing page template against NAR IDX policy checklist before launch |
|
||||
| Stale off-market listings | Listings sync infrastructure | Manually mark a test listing off-market in a dev MLS feed; verify it disappears from the site within 1 hour |
|
||||
| PDF heuristic field detection failure | PDF form preparation UI | Test against a scanned (non-OCR) Utah real estate form; verify the manual placement fallback UI appears |
|
||||
| Font not embedded before flattening | PDF form filling | Flatten a filled form in the production serverless environment; open the result in Acrobat and confirm all text is visible |
|
||||
| Mobile canvas unusable | Client signing UI | Complete a full signing flow on iOS Safari and Android Chrome on physical devices |
|
||||
| Missing MLS compliance disclaimers | Public listings feature | Legal/compliance checklist as a PR gate for any listing-display component |
|
||||
| iOS canvas self-clearing / vertical stroke bug | Client signing UI | Physical device testing on iOS 13 and iOS 15+ before any client-facing release |
|
||||
| Signed PDF IDOR (sequential IDs, no ownership check) | Document storage architecture | Penetration test with two accounts: confirm ID enumeration returns 403, not another user's document |
|
||||
| Next.js middleware-only auth (CVE-2025-29927) | API route implementation | Code review gate: every sensitive route handler must contain an explicit ownership/session check |
|
||||
| CFAA exposure from credential-based scraping | Forms import architecture design | Architecture decision document: no automated login code for utahrealestate.com; manual upload only |
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [ESIGN Act & UETA Compliance — UnicornForms](https://www.unicornforms.com/blog/esign-ueta-compliance)
|
||||
- [E-Signature Audit Trail Schema — Anvil Engineering](https://www.useanvil.com/blog/engineering/e-signature-audit-trail-schema-events-json-checklist/)
|
||||
- [Using E-Signatures in Court — Fenwick](https://www.fenwick.com/insights/publications/using-e-signatures-in-court-the-value-of-an-audit-trail)
|
||||
- [E-Signature Software with Audit Trail — Zignt](https://zignt.com/blog/e-signature-software-with-audit-trail)
|
||||
- [Utah Electronic Signature Act Explained — Signable](https://www.signable.co.uk/electronic-signatures-legally-binding-utah/)
|
||||
- [UtahRealEstate.com Data Services — Vendor FAQ](https://vendor.utahrealestate.com/faq)
|
||||
- [UtahRealEstate.com RESO Web API Docs](https://vendor.utahrealestate.com/webapi/docs/tuts/endpoints)
|
||||
- [WFRMLS IDX — Utah MLS at RESO](https://www.reso.org/web-api-examples/mls/utah-mls/)
|
||||
- [UtahRealEstate Partners with SkySlope Forms](https://blog.utahrealestate.com/index.php/2022/01/31/utahrealestate-comskyslope/)
|
||||
- [IDX Integration Best Practices — Real Estate 7](https://contempothemes.com/idx-integration-best-practices-for-mls-rules/)
|
||||
- [NAR IDX Policy Statement 7.58](https://www.nar.realtor/handbook-on-multiple-listing-policy/advertising-print-and-electronic-section-1-internet-data-exchange-idx-policy-policy-statement-7-58)
|
||||
- [Summary of 2025 MLS Changes — NAR](https://www.nar.realtor/about-nar/policies/summary-of-2025-mls-changes)
|
||||
- [PDF Page Coordinates — pdfscripting.com](https://www.pdfscripting.com/public/PDF-Page-Coordinates.cfm)
|
||||
- [PDF Coordinate Systems — Apryse](https://apryse.com/blog/pdf-coordinates-and-pdf-processing)
|
||||
- [Critical Bug: PDF Annotation Positioning Mismatch — sign-pdf GitHub issue](https://github.com/mattsilv/sign-pdf/issues/1)
|
||||
- [pdf-lib Field Coordinates Feature Request — GitHub issue #602](https://github.com/Hopding/pdf-lib/issues/602)
|
||||
- [Magic Link Security Best Practices — Deepak Gupta](https://guptadeepak.com/mastering-magic-link-security-a-deep-dive-for-developers/)
|
||||
- [Passwordless Magic Links: UX and Security Checklist — AppMaster](https://appmaster.io/blog/passwordless-magic-links-ux-security-checklist)
|
||||
- [Magic Link Security — BayTech Consulting](https://www.baytechconsulting.com/blog/magic-links-ux-security-and-growth-impacts-for-saas-platforms-2025)
|
||||
- [Signature Pad Library — szimek/signature_pad](https://github.com/szimek/signature_pad)
|
||||
- [MLS Listing Data Freshness and Cache — MLSImport](https://mlsimport.com/fix-outdated-listings-on-your-wordpress-real-estate-site/)
|
||||
- [Utah DRE State-Approved Forms — commerce.utah.gov](https://commerce.utah.gov/realestate/real-estate/forms/state-approved/)
|
||||
- [Canvas clears itself on iOS 15 (react-signature-canvas Issue #65) — GitHub](https://github.com/agilgur5/react-signature-canvas/issues/65)
|
||||
- [Drawing broken on iOS 13 (signature_pad Issue #455) — GitHub](https://github.com/szimek/signature_pad/issues/455)
|
||||
- [Free drawing on iOS disables page scrolling (fabric.js Issue #3756) — GitHub](https://github.com/fabricjs/fabric.js/issues/3756)
|
||||
- [Insecure Direct Object Reference Prevention — OWASP Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html)
|
||||
- [IDOR in Next.js / JavaScript Applications — nodejs-security.com](https://www.nodejs-security.com/blog/insecure-direct-object-reference-idor-javascript-applications)
|
||||
- [CVE-2025-29927: Next.js Middleware Authorization Bypass — ProjectDiscovery](https://projectdiscovery.io/blog/nextjs-middleware-authorization-bypass)
|
||||
- [Critical Next.js Vulnerability: Authorization Bypass in Middleware — Jit.io](https://www.jit.io/resources/app-security/critical-nextjs-vulnerability-authorization-bypass-in-middleware)
|
||||
- [U.S. Court Rules Against Online Travel Booking Company in Web-Scraping Case (2024 CFAA jury verdict) — Alston Bird](https://www.alstonprivacy.com/u-s-court-rules-against-online-travel-booking-company-in-web-scraping-case/)
|
||||
- [Web Scraping, website terms and the CFAA: hiQ affirmed — White & Case](https://www.whitecase.com/insight-our-thinking/web-scraping-website-terms-and-cfaa-hiqs-preliminary-injunction-affirmed-again)
|
||||
- [The Computer Fraud and Abuse Act and Third-Party Web Scrapers — Finnegan](https://www.finnegan.com/en/insights/articles/the-computer-fraud-and-abuse-act-and-third-party-web-scrapers.html)
|
||||
- [JWT Security Best Practices — Curity](https://curity.io/resources/learn/jwt-best-practices/)
|
||||
- [JSON Web Token Attacks — PortSwigger Web Security Academy](https://portswigger.net/web-security/jwt)
|
||||
- [PDF Form Flattening — DynamicPDF](https://www.dynamicpdf.com/docs/dotnet/dynamic-pdf-form-flattening)
|
||||
- [Flatten Your PDFs for Court Filings — eFilingHelp](https://www.efilinghelp.com/electronic-filing/flatten-pdfs/)
|
||||
- [E-Signatures in Real Estate Transactions: a Deep Dive — Gomez Law, APC](https://gomezlawla.com/blog/e-signatures-in-real-estate-transactions-a-deep-dive/)
|
||||
- [Enforceability of Electronic Agreements in Real Estate — Arnall Golden Gregory LLP](https://www.agg.com/news-insights/publications/enforceability-of-electronic-agreements-in-real-estate-transactions-06-30-2016/)
|
||||
- [Utah Code § 46-4-201: Legal recognition of electronic signatures — Justia](https://law.justia.com/codes/utah/title-46/chapter-4/part-2/section-201/)
|
||||
- [Utah Code § 46-4-203: Attribution and effect of electronic signature — Justia](https://law.justia.com/codes/utah/2023/title-46/chapter-4/part-2/section-203/)
|
||||
|
||||
---
|
||||
|
||||
*Pitfalls research for: Real estate broker web app — custom e-signature, WFRMLS/IDX integration, PDF document signing*
|
||||
*Researched: 2026-03-19*
|
||||
426
.planning/research/STACK.md
Normal file
426
.planning/research/STACK.md
Normal file
@@ -0,0 +1,426 @@
|
||||
# Stack Research
|
||||
|
||||
**Domain:** Real estate agent website + PDF document signing web app
|
||||
**Researched:** 2026-03-19
|
||||
**Confidence:** HIGH (versions verified via npm/GitHub; integration strategies based on official docs)
|
||||
|
||||
---
|
||||
|
||||
## Recommended Stack
|
||||
|
||||
### Core Technologies
|
||||
|
||||
| Technology | Version | Purpose | Why Recommended |
|
||||
|------------|---------|---------|-----------------|
|
||||
| Next.js | 15.5.x (use 15, not 16 — see note) | Full-stack framework | App Router + Server Components + API routes in one repo; Vercel-native; 15.5 is current LTS-equivalent with stable Node.js middleware |
|
||||
| React | 19.x | UI | Ships with Next.js 15; Server Components, `use()`, form actions all stable |
|
||||
| TypeScript | 5.x | Type safety | Required; Prisma, Auth.js, pdf-lib all ship full types |
|
||||
| PostgreSQL (Neon) | latest (PG 16) | Data persistence | Serverless-compatible, scales to zero, free tier generous, branches per PR — perfect for solo agent on Vercel |
|
||||
| Prisma ORM | 6.x (6.19+) | Database access | Best-in-class DX for TypeScript; schema migrations; works with Neon via `@neondatabase/serverless` driver |
|
||||
|
||||
**Note on Next.js version:** Next.js 16 is now available but introduced several breaking changes (sync APIs fully removed, `middleware` renamed to `proxy`, edge runtime dropped from proxy). Use **Next.js 15.5.x** for this project to avoid churn — it has stable Node.js middleware, stable Turbopack builds in beta, and typed routes. Upgrade to 16 after it stabilizes (6-12 months).
|
||||
|
||||
---
|
||||
|
||||
### PDF Processing Libraries
|
||||
|
||||
Three separate libraries serve three distinct roles. You need all three.
|
||||
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| `pdfjs-dist` | 5.5.x (5.5.207 current) | Render PDFs in browser; detect existing AcroForm fields | Use on the client side to display the imported PDF and to iterate over existing form field annotations so you can find where signature areas already exist |
|
||||
| `pdf-lib` | 1.17.1 (original) OR `@pdfme/pdf-lib` 5.5.x (actively maintained fork) | Modify PDFs server-side: fill text fields, embed signature images, flatten | Use on the server (API route / Server Action) to fill form fields, embed the canvas signature PNG, and produce the final signed PDF. Use the `@pdfme/pdf-lib` fork — the original has been unmaintained for 4 years |
|
||||
| `react-pdf` (wojtekmaj) | latest (wraps pdfjs-dist) | React component wrapper for PDF display | Use in the client-side signing UI to render the document page-by-page with minimal setup; falls back to `pdfjs-dist` for custom rendering needs |
|
||||
|
||||
**Critical distinction:**
|
||||
- `pdfjs-dist` = rendering/viewing only — cannot modify PDFs
|
||||
- `pdf-lib` / `@pdfme/pdf-lib` = modifying/creating PDFs — cannot render to screen
|
||||
- You must use both together for this workflow
|
||||
|
||||
---
|
||||
|
||||
### Supporting Libraries
|
||||
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| `better-auth` | 1.5.x (1.5.5 current) | Agent authentication | Single-agent portal; credentials auth (email+password) built in; rate limiting and session management out of the box; no per-MAU cost; first-class Next.js App Router support |
|
||||
| `resend` + `@react-email/components` | 6.9.x / latest | Email delivery for signing links | Resend is the modern Nodemailer replacement; generous free tier (3k/mo), React-based email templates, dead simple API route integration |
|
||||
| `signature_pad` | 5.1.3 | Canvas-based signature drawing | Core signature capture library; HTML5 canvas, Bezier curve smoothing, works on touch/mouse; use directly (not the React wrapper) so you control the ref and can export PNG |
|
||||
| `react-signature-canvas` | 1.1.0-alpha.2 | React wrapper around signature_pad | Optional convenience wrapper if you prefer JSX integration; note: alpha version — prefer using `signature_pad` directly with a `useRef` |
|
||||
| `@vercel/blob` | latest | PDF file storage | Zero-config for Vercel deploys; S3-backed (99.999999999% durability); PDFs are not large enough to hit egress cost issues for a solo agent; avoid vendor lock-in concern by abstracting behind a `storage.ts` service module |
|
||||
| `playwright` | 1.58.x | utahrealestate.com forms library scraping; credentials-based login | Only option for sites requiring JS execution + session cookies; multi-browser, auto-wait, built-in proxy support |
|
||||
| `zod` | 3.x | Request/form validation | Used with Server Actions and API routes; integrates with Prisma and better-auth |
|
||||
| `nanoid` | 5.x | Generating secure signing link tokens | Cryptographically secure, URL-safe, short IDs for signing request URLs |
|
||||
| `sharp` | 0.33.x | Image optimization if needed | Only needed if resizing/converting signature images before PDF embedding |
|
||||
|
||||
---
|
||||
|
||||
### Development Tools
|
||||
|
||||
| Tool | Purpose | Notes |
|
||||
|------|---------|-------|
|
||||
| `tailwindcss` v4 | Styling | v4 released 2025; CSS-native config, no `tailwind.config.js` required; significantly faster |
|
||||
| `shadcn/ui` | Component library | Copies components into your repo (not a dependency); works with Tailwind v4 and React 19; perfect for the agent portal UI |
|
||||
| ESLint 9 + `@typescript-eslint` | Linting | Next.js 15 ships ESLint 9 support |
|
||||
| Prettier | Formatting | Standard |
|
||||
| `prisma studio` | Database GUI | Built into Prisma; `npx prisma studio` |
|
||||
| `@next/bundle-analyzer` | Bundle analysis | Experimental in Next.js 16.1 but available as standalone package for 15 |
|
||||
| Turbopack | Dev server | Enabled by default in Next.js 15 (`next dev --turbo`); production builds still use Webpack in 15.x |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Core framework
|
||||
npm install next@^15.5 react@^19 react-dom@^19
|
||||
|
||||
# Database
|
||||
npm install prisma @prisma/client @neondatabase/serverless
|
||||
|
||||
# Authentication
|
||||
npm install better-auth
|
||||
|
||||
# PDF processing
|
||||
npm install @pdfme/pdf-lib pdfjs-dist react-pdf
|
||||
|
||||
# E-signature (canvas)
|
||||
npm install signature_pad
|
||||
|
||||
# Email
|
||||
npm install resend @react-email/components react-email
|
||||
|
||||
# File storage
|
||||
npm install @vercel/blob
|
||||
|
||||
# Scraping (for utahrealestate.com forms + listings)
|
||||
npm install playwright
|
||||
|
||||
# Utilities
|
||||
npm install zod nanoid
|
||||
|
||||
# Dev dependencies
|
||||
npm install -D typescript @types/node @types/react @types/react-dom
|
||||
npm install -D tailwindcss @tailwindcss/postcss postcss
|
||||
npm install -D eslint eslint-config-next @typescript-eslint/eslint-plugin prettier
|
||||
npx prisma init
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
| Recommended | Alternative | Why Not |
|
||||
|-------------|-------------|---------|
|
||||
| `better-auth` | Clerk | Clerk costs per MAU; overkill for one agent; hosted user data is unnecessary complexity for a single known user |
|
||||
| `better-auth` | Auth.js v5 (NextAuth) | Auth.js v5 remains in beta; better-auth has the same open-source no-lock-in benefit but with built-in rate limiting and MFA. Notably, Auth.js is merging with better-auth team |
|
||||
| `@pdfme/pdf-lib` | original `pdf-lib` | Original pdf-lib (v1.17.1) last published 4 years ago; `@pdfme/pdf-lib` fork is actively maintained with bug fixes |
|
||||
| `resend` | Nodemailer + SMTP | Nodemailer requires managing an SMTP server or credentials; Resend has better deliverability, a dashboard, and React template support |
|
||||
| Neon PostgreSQL | PlanetScale / Supabase | PlanetScale killed its free tier; Supabase is excellent but heavier than needed; Neon's Vercel integration and branching per PR is the best solo developer experience |
|
||||
| Neon PostgreSQL | SQLite (local) | SQLite doesn't work on Vercel serverless; fine for local dev but you'd need to swap before deploying — adds friction. Neon's free tier makes this unnecessary |
|
||||
| `playwright` | Puppeteer | Playwright has better auto-wait, multi-browser, and built-in proxy support; more actively maintained for 2025 use cases |
|
||||
| `playwright` | Cheerio | utahrealestate.com requires authentication (login session) and renders content with JavaScript; Cheerio only parses static HTML |
|
||||
| `@vercel/blob` | AWS S3 | S3 requires IAM setup, bucket policies, and CORS config; Vercel Blob is zero-config for Vercel deploys and S3-backed under the hood. Abstract it behind a service module so you can swap later |
|
||||
| `signature_pad` | react-signature-canvas | The React wrapper is at `1.1.0-alpha.2` — alpha status; better to use the underlying `signature_pad@5.1.3` directly with a `useRef` canvas hook |
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Use
|
||||
|
||||
| Avoid | Why | Use Instead |
|
||||
|-------|-----|-------------|
|
||||
| DocuSign / HelloSign | Monthly subscription cost for a solo agent; defeats the purpose of a custom tool | Custom `signature_pad` canvas capture embedded via `@pdfme/pdf-lib` |
|
||||
| Apryse WebViewer / Nutrient SDK | Enterprise pricing ($$$); overkill; hides the PDF internals you need to control | `pdfjs-dist` (render) + `@pdfme/pdf-lib` (modify) |
|
||||
| `jspdf` | Creates PDFs from scratch via HTML canvas; cannot modify existing PDFs | `@pdfme/pdf-lib` for modification |
|
||||
| `pdfmake` | Same limitation as jspdf; generates new PDFs, can't edit existing form fields | `@pdfme/pdf-lib` |
|
||||
| Firebase / Supabase Storage | Additional vendor; Vercel Blob is already in the stack | `@vercel/blob` |
|
||||
| Clerk | Per-MAU pricing; vendor-hosted user data; one agent user doesn't need this complexity | `better-auth` |
|
||||
| Next.js 16 (right now) | Too many breaking changes (async APIs fully enforced, middleware renamed); ecosystem compatibility issues | Next.js 15.5.x |
|
||||
| WebSockets for signing | Overkill; the signing flow is a one-shot action, not a live collaboration session | Server Actions + polling or simple page refresh |
|
||||
|
||||
---
|
||||
|
||||
## PDF Processing Strategy
|
||||
|
||||
### Overview
|
||||
|
||||
The workflow has five distinct stages, each using different tools:
|
||||
|
||||
```
|
||||
[Import PDF] → [Detect fields] → [Add signature areas] → [Fill + sign] → [Store signed PDF]
|
||||
Playwright pdfjs-dist @pdfme/pdf-lib @pdfme/pdf-lib @vercel/blob
|
||||
```
|
||||
|
||||
### Stage 1: Importing PDFs from utahrealestate.com
|
||||
|
||||
The forms library on utahrealestate.com requires agent login. Use **Playwright** on the server (a Next.js API route or background job) to:
|
||||
1. Authenticate with the agent's saved credentials (stored encrypted in the DB)
|
||||
2. Navigate to the forms library
|
||||
3. Download the target PDF as a Buffer
|
||||
4. Store the original PDF in Vercel Blob under a UUID-based path
|
||||
5. Record the document in the database with its Blob URL, filename, and source metadata
|
||||
|
||||
Playwright should run in a separate process or as a scheduled job (Vercel Cron), not inline with a user request, because browser startup is slow.
|
||||
|
||||
### Stage 2: Detecting Existing Form Fields
|
||||
|
||||
Once the PDF is stored, use **`pdfjs-dist`** in a server-side script (Node.js API route) to:
|
||||
1. Load the PDF from Blob storage
|
||||
2. Iterate over each page's annotations
|
||||
3. Find `Widget` annotations (AcroForm fields: text inputs, checkboxes, signature fields)
|
||||
4. Record each field's name, type, and bounding box (x, y, width, height, page number)
|
||||
5. Store this field map in the database (`DocumentField` table)
|
||||
|
||||
Utah real estate forms from WFRMLS are typically standard AcroForm PDFs with pre-defined fields. Most will have existing form fields you can fill directly.
|
||||
|
||||
### Stage 3: Adding Signature Areas
|
||||
|
||||
For pages that lack signature fields (or when the agent wants to add a new signature area):
|
||||
- Use **`@pdfme/pdf-lib`** server-side to add a new AcroForm signature annotation at a specified bounding box
|
||||
- Alternatively, track "signature zones" as metadata in your database (coordinates + page) and overlay them on the rendering side — this avoids PDF modification until signing time
|
||||
|
||||
The simpler approach: store signature zones as coordinate records in the DB, render them as highlighted overlay boxes in the browser using `react-pdf`, and embed the actual signature image at those coordinates only at signing time.
|
||||
|
||||
### Stage 4: Filling Text Fields + Embedding Signature
|
||||
|
||||
When the agent fills out a document form (pre-filling client info, property address, etc.):
|
||||
1. Send field values to a Server Action
|
||||
2. Server Action loads the PDF from Blob
|
||||
3. Use **`@pdfme/pdf-lib`** to:
|
||||
- Get the AcroForm from the PDF
|
||||
- Set text field values: `form.getTextField('BuyerName').setText(value)`
|
||||
- Embed the signature PNG: convert the canvas `toDataURL()` PNG to a `Uint8Array`, embed as `PDFImage`, draw it at the signature zone coordinates
|
||||
- Optionally flatten the form (make fields non-editable) before final storage
|
||||
4. Save the modified PDF bytes back to Blob as a new file (preserve the unsigned original)
|
||||
|
||||
### Stage 5: Storing Signed PDFs
|
||||
|
||||
Store three versions in Vercel Blob:
|
||||
- `/documents/{id}/original.pdf` — the untouched import
|
||||
- `/documents/{id}/prepared.pdf` — fields filled, ready to sign
|
||||
- `/documents/{id}/signed.pdf` — final document with embedded signature
|
||||
|
||||
All three paths recorded in the `Document` table. Serve signed PDFs via signed Blob URLs with short expiry (1 hour) to prevent unauthorized access.
|
||||
|
||||
---
|
||||
|
||||
## E-Signature Legal Requirements
|
||||
|
||||
Under the ESIGN Act (federal) and UETA (47 states), an electronic signature is legally valid when it demonstrates: **intent to sign**, **consent to transact electronically**, **association of the signature with the record**, and **reliable record retention**.
|
||||
|
||||
### What to Capture at Signing Time
|
||||
|
||||
Store all of the following in an `AuditEvent` table linked to the `SigningRequest`:
|
||||
|
||||
| Data Point | How to Capture | Legal Purpose |
|
||||
|------------|---------------|---------------|
|
||||
| Signer's IP address | `request.headers.get('x-forwarded-for')` in API route | Attribution — links signature to a network location |
|
||||
| Timestamp (UTC) | `new Date().toISOString()` server-side | Proves the signature occurred at a specific time |
|
||||
| User-Agent string | `request.headers.get('user-agent')` | Device/browser fingerprint |
|
||||
| Consent acknowledgment | Require checkbox click: "I agree to sign electronically" | Explicit ESIGN/UETA consent requirement |
|
||||
| Document hash (pre-sign) | `crypto.subtle.digest('SHA-256', pdfBytes)` | Proves document was not altered before signing |
|
||||
| Document hash (post-sign) | Same, after embedding signature | Proves final document integrity |
|
||||
| Signing link token | The `nanoid`-generated token used to access the signing page | Ties the signer to the specific invitation |
|
||||
| Email used for invitation | From the `SigningRequest` record | Identity association |
|
||||
|
||||
### Signing Audit Trail Schema (minimal)
|
||||
|
||||
```typescript
|
||||
// Prisma model
|
||||
model AuditEvent {
|
||||
id String @id @default(cuid())
|
||||
signingRequestId String
|
||||
signingRequest SigningRequest @relation(fields: [signingRequestId], references: [id])
|
||||
eventType String // "viewed" | "consent_given" | "signed" | "downloaded"
|
||||
ipAddress String
|
||||
userAgent String
|
||||
timestamp DateTime @default(now())
|
||||
metadata Json? // document hashes, page count, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### Signing Page Flow
|
||||
|
||||
1. Client opens the email link containing a `nanoid` token
|
||||
2. Server validates the token (not expired, not already used)
|
||||
3. Record `"viewed"` audit event (IP, timestamp, UA)
|
||||
4. Client sees a consent banner: "By signing, you agree to execute this document electronically under the ESIGN Act."
|
||||
5. Client checks consent checkbox — record `"consent_given"` audit event
|
||||
6. Client draws signature on canvas
|
||||
7. On submit: POST to API route with canvas PNG data URL
|
||||
8. Server records `"signed"` audit event, embeds signature into PDF, stores signed PDF
|
||||
9. Mark signing request as complete; email confirmation to both agent and client
|
||||
|
||||
### What This Does NOT Cover
|
||||
|
||||
This custom implementation is sufficient for standard real estate transactions in Utah under UETA. However:
|
||||
- It does NOT provide notarization (RON — Remote Online Notarization is a separate regulated process)
|
||||
- It does NOT provide RFC 3161 trusted timestamps (requires a TSA — unnecessary for most residential RE transactions)
|
||||
- For purchase agreements and disclosures (standard Utah REPC forms), this level of e-signature is accepted by WFRMLS and Utah law
|
||||
|
||||
---
|
||||
|
||||
## utahrealestate.com Integration Strategy
|
||||
|
||||
### Two Separate Use Cases
|
||||
|
||||
**Use Case 1: Forms Library (PDF import)**
|
||||
The forms library requires agent login. There is no documented public API for downloading forms. Strategy:
|
||||
1. Store the agent's utahrealestate.com credentials encrypted in the database (use `bcrypt` or AES-256-GCM encryption with a server-side secret key — not bcrypt since you need to recover the plaintext)
|
||||
2. Use Playwright to authenticate: navigate to the login page, fill credentials, submit, wait for session cookies
|
||||
3. Navigate to the forms section, find the desired form by name/category, download the PDF
|
||||
4. This is fragile (subject to site redesigns) but unavoidable without official API access
|
||||
5. Implement with a health check: if Playwright fails to find expected elements, send the agent an alert email via Resend
|
||||
|
||||
**Use Case 2: Listings Display (MLS data)**
|
||||
WFRMLS provides an official RESO OData API at `https://resoapi.utahrealestate.com/reso/odata/`. Access requires:
|
||||
1. Apply for licensed data access at [vendor.utahrealestate.com](https://vendor.utahrealestate.com) ($50 IDX enrollment fee)
|
||||
2. Obtain a Bearer Token (OAuth-based)
|
||||
3. Query the `Property` resource using OData syntax
|
||||
|
||||
Example query (fetch recent active listings):
|
||||
```
|
||||
GET https://resoapi.utahrealestate.com/reso/odata/Property
|
||||
?$filter=StandardStatus eq 'Active'
|
||||
&$orderby=ModificationTimestamp desc
|
||||
&$top=20
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
4. Cache results in the database or in-memory (listings don't change by the second) using Next.js `unstable_cache` or a simple `revalidate` tag
|
||||
5. Display on the public marketing site — no authentication required to view
|
||||
|
||||
**Do not scrape the listing pages directly.** WFRMLS terms of service prohibit unauthorized data extraction, and the official API path is straightforward for a licensed agent.
|
||||
|
||||
### Playwright Implementation Notes
|
||||
|
||||
Run Playwright scraping jobs via:
|
||||
- **Vercel Cron** for scheduled form sync (daily refresh of available forms list)
|
||||
- **On-demand API route** when the agent requests a specific form download
|
||||
- Use `playwright-core` + a managed browser service (Browserless.io free tier, or self-hosted Chromium on a small VPS) for Vercel compatibility — Vercel serverless functions cannot run a full Playwright browser due to size limits
|
||||
|
||||
Alternatively, if the Vercel function size is a concern, extract the Playwright logic into a separate lightweight service (a small Express app on a $5/month VPS, or a Railway.app container) and call it from your Next.js API routes.
|
||||
|
||||
---
|
||||
|
||||
## MLS/WFRMLS Listings Display
|
||||
|
||||
Once you have the RESO OData token, the listings page on the public marketing site is straightforward:
|
||||
|
||||
1. **Server Component** fetches listings from the RESO API (or from a cached DB table)
|
||||
2. Display property cards with photo, price, address, beds/baths
|
||||
3. Photos: WFRMLS media URLs are served directly; use `next/image` with the MLS domain whitelisted in `next.config.ts`
|
||||
4. Detail page: dynamic route `/listings/[mlsNumber]` with `generateStaticParams` for ISR (revalidate every hour)
|
||||
5. No client-side JavaScript needed for browsing — pure Server Components + Suspense
|
||||
|
||||
---
|
||||
|
||||
## Database Schema (Key Tables)
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
// better-auth manages password hashing
|
||||
createdAt DateTime @default(now())
|
||||
clients Client[]
|
||||
documents Document[]
|
||||
}
|
||||
|
||||
model Client {
|
||||
id String @id @default(cuid())
|
||||
agentId String
|
||||
agent User @relation(fields: [agentId], references: [id])
|
||||
name String
|
||||
email String
|
||||
phone String?
|
||||
createdAt DateTime @default(now())
|
||||
signingRequests SigningRequest[]
|
||||
}
|
||||
|
||||
model Document {
|
||||
id String @id @default(cuid())
|
||||
agentId String
|
||||
agent User @relation(fields: [agentId], references: [id])
|
||||
title String
|
||||
originalBlobUrl String
|
||||
preparedBlobUrl String?
|
||||
signedBlobUrl String?
|
||||
status String // "draft" | "sent" | "signed"
|
||||
fields DocumentField[]
|
||||
signingRequests SigningRequest[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model DocumentField {
|
||||
id String @id @default(cuid())
|
||||
documentId String
|
||||
document Document @relation(fields: [documentId], references: [id])
|
||||
fieldName String
|
||||
fieldType String // "text" | "checkbox" | "signature"
|
||||
page Int
|
||||
x Float
|
||||
y Float
|
||||
width Float
|
||||
height Float
|
||||
value String?
|
||||
}
|
||||
|
||||
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])
|
||||
token String @unique // nanoid for the signing URL
|
||||
expiresAt DateTime
|
||||
signedAt DateTime?
|
||||
status String // "pending" | "signed" | "expired"
|
||||
auditEvents AuditEvent[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model AuditEvent {
|
||||
id String @id @default(cuid())
|
||||
signingRequestId String
|
||||
signingRequest SigningRequest @relation(fields: [signingRequestId], references: [id])
|
||||
eventType String // "viewed" | "consent_given" | "signed"
|
||||
ipAddress String
|
||||
userAgent String
|
||||
timestamp DateTime @default(now())
|
||||
metadata Json?
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Next.js 15.5 Release Notes](https://nextjs.org/blog/next-15-5) — HIGH confidence
|
||||
- [Next.js 16 Upgrade Guide](https://nextjs.org/docs/app/guides/upgrading/version-16) — HIGH confidence (confirms 16 is available but breaking)
|
||||
- [pdf-lib npm page](https://www.npmjs.com/package/pdf-lib) — HIGH confidence (v1.17.1, unmaintained 4 years)
|
||||
- [@pdfme/pdf-lib npm page](https://www.npmjs.com/package/@pdfme/pdf-lib) — HIGH confidence (v5.5.8, actively maintained fork)
|
||||
- [pdfjs-dist npm / libraries.io](https://libraries.io/npm/pdfjs-dist) — HIGH confidence (v5.5.207 current)
|
||||
- [JavaScript PDF Libraries Comparison 2025 (Nutrient)](https://www.nutrient.io/blog/javascript-pdf-libraries/) — HIGH confidence
|
||||
- [signature_pad npm](https://www.npmjs.com/package/signature_pad) — HIGH confidence (v5.1.3)
|
||||
- [react-signature-canvas npm](https://www.npmjs.com/package/react-signature-canvas) — HIGH confidence (v1.1.0-alpha.2, alpha status noted)
|
||||
- [better-auth npm](https://www.npmjs.com/package/better-auth) — HIGH confidence (v1.5.5)
|
||||
- [Auth.js joins better-auth discussion](https://github.com/nextauthjs/next-auth/discussions/13252) — HIGH confidence
|
||||
- [better-auth Next.js integration](https://better-auth.com/docs/integrations/next) — HIGH confidence
|
||||
- [resend npm](https://www.npmjs.com/package/resend) — HIGH confidence (v6.9.4)
|
||||
- [Prisma 6.19.0 announcement](https://www.prisma.io/blog/announcing-prisma-6-19-0) — HIGH confidence
|
||||
- [Neon + Vercel integration](https://vercel.com/marketplace/neon) — HIGH confidence
|
||||
- [playwright npm](https://www.npmjs.com/package/playwright) — HIGH confidence (v1.58.2)
|
||||
- [UtahRealEstate.com Vendor Data Services](https://vendor.utahrealestate.com/) — HIGH confidence (official RESO OData API confirmed)
|
||||
- [RESO OData endpoints for WFRMLS](https://vendor.utahrealestate.com/webapi/docs/tuts/endpoints) — HIGH confidence
|
||||
- [ESIGN/UETA audit trail requirements (Anvil Engineering)](https://www.useanvil.com/blog/engineering/e-signature-audit-trail-schema-events-json-checklist/) — HIGH confidence
|
||||
- [E-signature legal requirements (BlueNotary)](https://bluenotaryonline.com/electronic-signature-legal-requirements/) — HIGH confidence
|
||||
- [Vercel Blob documentation](https://vercel.com/docs/vercel-blob) — HIGH confidence
|
||||
- [Playwright vs Puppeteer 2025 (BrowserStack)](https://www.browserstack.com/guide/playwright-vs-puppeteer) — HIGH confidence
|
||||
- [NextAuth vs Clerk vs better-auth comparison (supastarter)](https://supastarter.dev/blog/better-auth-vs-nextauth-vs-clerk) — MEDIUM confidence (third-party analysis)
|
||||
|
||||
---
|
||||
|
||||
*Stack research for: Teressa Copeland Homes — real estate agent website + document signing*
|
||||
*Researched: 2026-03-19*
|
||||
251
.planning/research/SUMMARY.md
Normal file
251
.planning/research/SUMMARY.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# Project Research Summary
|
||||
|
||||
**Project:** Teressa Copeland Homes
|
||||
**Domain:** Real estate agent marketing site + custom PDF document signing portal (Utah/WFRMLS)
|
||||
**Researched:** 2026-03-19
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This is a dual-product build: a public real estate marketing site for a solo Utah agent, and a private document-signing portal that replaces per-month third-party tools (DocuSign, HelloSign) with a fully branded, custom implementation. Research across all four domains converges on a single-repo Next.js 15 application deployed to Vercel with a Neon PostgreSQL database and Vercel Blob storage for PDFs. The stack is unambiguous: `pdfjs-dist` for browser PDF rendering, `@pdfme/pdf-lib` (the maintained fork) for server-side PDF modification, `signature_pad` for canvas signature capture, `better-auth` for agent authentication, and `resend` for email delivery of signing links. This combination is well-documented, actively maintained, and sized exactly right for a solo-agent workflow — no enterprise licensing, no per-user cost, and full brand control throughout.
|
||||
|
||||
The recommended architecture cleanly separates two subsystems that share only the Next.js shell: the public marketing site (listings, bio, contact) and the protected agent portal (clients, documents, signing requests). These can be built in parallel after the foundation is in place and are only loosely coupled — the listings page has no dependency on the signing flow, and vice versa. The five-stage PDF pipeline (import → parse fields → fill text → send for signing → embed signature) maps directly to a sequential build order where each stage unblocks the next. The architecture research prescribes a 7-phase build order that aligns exactly with this dependency chain.
|
||||
|
||||
The most serious risks in this domain are legal, not technical. A custom e-signature implementation that lacks a tamper-evident PDF hash, an incomplete audit trail, or replayable signing links is legally indefensible under ESIGN/UETA regardless of how well the rest of the app works. Pitfalls research is unambiguous: audit logging and one-time token enforcement must be built in from the first signing ceremony — they cannot be retrofitted. A second cluster of risk sits in the WFRMLS integration: scraping the utahrealestate.com forms library violates Terms of Service (the platform partners with SkySlope; no public API exists for forms), and displaying IDX listing data without required broker attribution and NAR-mandated disclaimer text risks fines and loss of MLS access. Both risks are avoidable with correct design decisions made before code is written.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### Recommended Stack
|
||||
|
||||
The stack is cohesive and all components are current-stable. Next.js 15.5.x (not 16 — too many breaking changes just landed) with React 19 provides the full-stack framework, App Router route groups for clean public/agent separation, and Server Components for the listings page without client-side JavaScript. Neon PostgreSQL with Prisma 6.x is the data layer — serverless-compatible, branches per PR, and generous free tier. Authentication uses `better-auth` 1.5.x (Auth.js/NextAuth is merging with better-auth and remains in beta; Clerk has per-MAU pricing that makes no sense for a single agent). PDF processing requires three distinct libraries that serve non-overlapping roles: `pdfjs-dist` for browser rendering and field detection (cannot modify PDFs), `@pdfme/pdf-lib` for server-side modification and signature embedding (cannot render to screen), and `signature_pad` for canvas capture. The original `pdf-lib` is unmaintained (4 years since last publish) — use the `@pdfme/pdf-lib` fork exclusively.
|
||||
|
||||
**Core technologies:**
|
||||
- **Next.js 15.5.x**: Full-stack framework — App Router, Server Components, API routes in one repo; Vercel-native; skip v16 until ecosystem stabilizes
|
||||
- **React 19**: Ships with Next.js 15; Server Components and form actions stable
|
||||
- **TypeScript 5.x**: Required; all major libraries ship full types
|
||||
- **Neon PostgreSQL + Prisma 6.x**: Serverless-compatible DB; best-in-class DX for TypeScript; Vercel Marketplace integration
|
||||
- **better-auth 1.5.x**: No per-MAU cost; built-in rate limiting; first-class Next.js App Router support
|
||||
- **pdfjs-dist 5.5.x**: Browser-side PDF rendering and AcroForm field detection only
|
||||
- **@pdfme/pdf-lib 5.5.x**: Server-side PDF modification — fill fields, embed signature PNG, flatten; actively maintained fork
|
||||
- **signature_pad 5.1.3**: Canvas signature capture; Bezier smoothing; touch-normalized; use directly (not the alpha React wrapper)
|
||||
- **resend + @react-email/components**: Transactional email for signing links; 3k/month free tier; React email templates
|
||||
- **@vercel/blob**: Zero-config PDF storage for Vercel; S3-backed; abstract behind `storage.ts` to allow future swap
|
||||
- **Playwright**: Required for utahrealestate.com forms (if used); run via separate service or Browserless.io — not inline in Vercel serverless
|
||||
- **Tailwind CSS v4 + shadcn/ui**: Styling; Tailwind v4 is CSS-native, no config file required
|
||||
|
||||
**Critical version note:** Do NOT use `next@16` — breaking changes (async APIs, middleware renamed to proxy, edge runtime dropped) make it incompatible with the ecosystem today. Use `next@15.5.x` and revisit in 6-12 months.
|
||||
|
||||
### Expected Features
|
||||
|
||||
Research identifies a clear MVP boundary. The signing portal is the novel differentiator — all the marketing site features are table stakes for any professional real estate presence. The key UX insight from feature research: no client account creation, ever. The signing link token is the client's identity. Every friction point between "email received" and "signature captured" is an abandonment driver.
|
||||
|
||||
**Must have for v1 launch (P1):**
|
||||
- Marketing site: hero with agent photo, bio, contact form, testimonials
|
||||
- Active listings display from WFRMLS (with full IDX compliance: broker attribution, disclaimer text, last-updated timestamp)
|
||||
- Agent login (better-auth, credentials)
|
||||
- Client management: create/view clients with name and email
|
||||
- PDF upload and browser rendering (pdfjs-dist + react-pdf)
|
||||
- Signature field placement UI: drag-and-drop on PDF canvas with coordinate storage in PDF user space
|
||||
- Email delivery of unique, tokenized signing link (resend)
|
||||
- Token-based anonymous client signing page: no account, no login
|
||||
- Canvas signature capture (signature_pad) — mobile-first, touch-action:none, iOS Safari tested
|
||||
- Audit trail: IP, timestamp, user-agent, consent acknowledgment — all server-side timestamps
|
||||
- PDF signature embed + form flatten with fonts embedded before flattening (@pdfme/pdf-lib)
|
||||
- Tamper-evident PDF hash: SHA-256 of final signed PDF bytes stored in DB
|
||||
- One-time signing token enforcement: used_at column, DB transaction, 72-hour TTL
|
||||
- Signed document storage (Vercel Blob, private ACLs, presigned URLs only)
|
||||
- Agent dashboard: document status at a glance (Draft / Sent / Viewed / Signed)
|
||||
|
||||
**Should have, add after v1 (P2):**
|
||||
- Forms library import from utahrealestate.com — add only as manual agent upload, not scraping
|
||||
- Heuristic AcroForm field detection on Utah standard forms — manual placement fallback always present
|
||||
- Document view/open tracking (link opened audit event)
|
||||
- Signed document confirmation email to client
|
||||
- Multiple field types: initials, auto-date, checkbox, text inputs
|
||||
- Neighborhood guide / SEO content pages
|
||||
|
||||
**Defer to v2+ (P3):**
|
||||
- Automated unsigned-document reminder system (requires scheduling infrastructure)
|
||||
- Client portal with document history (requires client accounts — explicitly an anti-feature in v1)
|
||||
- Multi-agent / brokerage support (role/permissions model doubles auth complexity)
|
||||
- Bulk document send
|
||||
- Native mobile app (responsive web signing works; 50%+ of e-signatures happen on mobile web)
|
||||
|
||||
**Anti-features to avoid entirely:**
|
||||
- Scraping utahrealestate.com forms library (ToS violation; use manual upload)
|
||||
- DocuSign/HelloSign integration (monthly cost, third-party branding — defeats the purpose)
|
||||
- In-app PDF content editing (real estate contracts have legally mandated language; editing creates liability)
|
||||
- WebSockets for signing (one-shot action, not a live session)
|
||||
- Client account creation for signing (friction kills completion rates)
|
||||
|
||||
### Architecture Approach
|
||||
|
||||
The system is a single Next.js monorepo with two distinct route groups: `(public)` for the unauthenticated marketing site and `(agent)` for the protected portal. These share the Prisma/Neon data layer and Vercel Blob storage but have no UI coupling. The PDF pipeline is entirely server-side (API routes / Server Actions) except for browser rendering and signature capture, which require Client Components loaded with `dynamic(() => import(...), { ssr: false })`. Authentication uses three defense-in-depth layers — edge middleware, layout Server Component, and per-route handler — because the CVE-2025-29927 middleware bypass (disclosed March 2025) demonstrated that middleware alone is not sufficient. The signing flow uses JWT tokens (HMAC-SHA256) stored server-side with one-time enforcement via a `used_at` column, providing both stateless verification and replay protection.
|
||||
|
||||
**Major components and responsibilities:**
|
||||
|
||||
1. **`middleware.ts`** — Edge auth gate for `/agent/*`; redirects to login; NOT the only auth layer (CVE-2025-29927 requires defense in depth)
|
||||
2. **`(agent)/layout.tsx`** — Server Component second auth layer; calls `verifySession()` on every render
|
||||
3. **`lib/pdf/parse.ts`** — pdfjs-dist server-side: extracts AcroForm field names, types, and bounding boxes from PDF bytes
|
||||
4. **`lib/pdf/fill.ts`** — @pdfme/pdf-lib server-side: writes agent-supplied text into named form fields; embeds fonts before flattening
|
||||
5. **`lib/pdf/sign.ts`** — @pdfme/pdf-lib server-side: embeds signature PNG at stored coordinates; flattens and seals the form; computes SHA-256 hash
|
||||
6. **`components/agent/PDFFieldMapper.tsx`** — Client Component: renders PDF page via canvas; drag-to-define signature zones; converts viewport coordinates to PDF user space (origin bottom-left) before storing
|
||||
7. **`components/sign/SignatureCanvas.tsx`** — Client Component: signature_pad with touch-action:none; exports PNG for submission
|
||||
8. **`app/sign/[token]/page.tsx`** — Public signing page: validates JWT, streams prepared PDF, renders field overlays, submits canvas PNG
|
||||
9. **`lib/wfrmls/client.ts`** — WFRMLS RESO OData client with ISR revalidate=3600 for listings
|
||||
10. **`lib/storage/s3.ts`** — Vercel Blob abstraction: upload, download, presigned URL (5-minute TTL); PDFs never served with public ACLs
|
||||
|
||||
**Database schema highlights:** `User` → `Client` → `SigningRequest` → `SignatureAuditLog` chain. `Document` stores the raw PDF template (reusable across multiple `SigningRequest`s). `SigningRequest` holds `fieldValues`, `signatureFields` (coordinates in PDF space), `preparedS3Key`, `signedS3Key`, `token`, `tokenJti`, `usedAt`, and status enum (DRAFT/SENT/VIEWED/SIGNED/EXPIRED/CANCELLED). `SignatureAuditLog` is append-only and records every ceremony event with server-side timestamps.
|
||||
|
||||
**PDF coordinate conversion is mandatory:** Browser canvas coordinates (origin top-left, Y down) must be converted to PDF user space (origin bottom-left, Y up) using `viewport.convertToPdfPoint(x, y)` from pdfjs-dist before storage. This conversion must be unit-tested against actual Utah real estate forms before the field placement UI ships.
|
||||
|
||||
### Critical Pitfalls
|
||||
|
||||
1. **No tamper-evident PDF hash** — Compute SHA-256 of the complete signed PDF bytes immediately after embedding the signature, before storing. Store the hash in `SigningRequest`. This is the difference between a legally defensible document and "a drawing on a page."
|
||||
|
||||
2. **Incomplete audit trail** — Log six ceremony events server-side: document prepared, email sent, link opened (with IP/UA/timestamp), document viewed, signature submitted, final PDF hash computed. Timestamps must be server-side — client-reported timestamps are legally worthless. This must be wired in before the first signing ceremony, not added later.
|
||||
|
||||
3. **Replayable signing token** — Signing tokens must have a `used_at` column set atomically on successful submission (DB transaction to prevent race conditions). After use, the link must return "already signed" — never the canvas. 72-hour TTL is appropriate for real estate (clients don't check email instantly; 15-minute magic-link windows are too short).
|
||||
|
||||
4. **PDF coordinate system mismatch** — PDF user space uses bottom-left origin with Y increasing upward; browser canvas uses top-left with Y downward. Embedding a signature without the conversion inverts the Y position. Write a coordinate conversion unit test against a real Utah purchase agreement form before building the drag-and-drop UI. Also handle page rotation (`Rotate` key in PDF) — Utah forms may be rotated 90 degrees.
|
||||
|
||||
5. **utahrealestate.com forms scraping violates ToS** — The platform partners with SkySlope and has no public forms API. Storing Teressa's credentials to automate downloads violates the WFRMLS data licensing agreement. Use manual PDF upload as the document source. State-approved Utah DRE forms at commerce.utah.gov are public domain and can be embedded directly.
|
||||
|
||||
6. **IDX compliance violations** — Every listing page (card and detail) must display: listing broker/office name (`ListOfficeName`), WFRMLS disclaimer text verbatim, last-updated timestamp from the feed, and buyer's agent compensation disclosure per 2024 NAR settlement. Missing any of these risks fines up to $15,000 and loss of MLS access. Treat these as acceptance criteria, not post-launch polish.
|
||||
|
||||
7. **PDF font flattening failure** — AcroForm fields reference fonts by name (e.g., "Helvetica"). On Vercel serverless, no system fonts are installed. Calling `form.flatten()` without first embedding fonts produces blank text fields in the downloaded PDF. Explicitly embed standard fonts from pdf-lib's built-in set on every form field before flattening. Validate in production environment, not just local Mac with system fonts.
|
||||
|
||||
8. **Mobile canvas scrolling instead of signing** — Without `touch-action: none` on the canvas element, iOS Safari and Android Chrome intercept touch gestures as page scroll. The client tries to sign and the page scrolls instead. This must be tested on physical devices before any client is sent a signing link.
|
||||
|
||||
## Implications for Roadmap
|
||||
|
||||
Based on the dependency chain identified in architecture research and the feature priority matrix from feature research, the natural build order is 7 phases:
|
||||
|
||||
### Phase 1: Foundation
|
||||
**Rationale:** Everything else depends on this. Database schema, auth system, and storage infrastructure must exist before any feature can be built. Auth has a well-documented security pattern (three-layer defense-in-depth due to CVE-2025-29927) that must be established upfront — retrofitting auth is expensive.
|
||||
**Delivers:** Working Next.js project, Prisma schema + Neon DB, Vercel Blob bucket, better-auth credentials login, middleware + layout guard, agent can log in and reach a blank dashboard.
|
||||
**Addresses:** Agent login (P1 feature), tamper-resistant auth architecture.
|
||||
**Avoids:** Middleware-only auth bypass (CVE-2025-29927 defense-in-depth established from day one).
|
||||
**Research flag:** None needed — well-documented patterns for all components.
|
||||
|
||||
### Phase 2: Public Marketing Site
|
||||
**Rationale:** Independent of the document workflow; provides immediate business value; unblocks IDX integration testing early. Can be built by a different person in parallel with Phase 3 if needed.
|
||||
**Delivers:** Public-facing site with hero, bio, contact form, and WFRMLS listings display with full IDX compliance (broker attribution, disclaimer, last-updated, NAR 2024 compensation disclosure).
|
||||
**Addresses:** Hero/bio/contact (P1), listings display (P1), IDX compliance (legal requirement), delta-sync listings refresh.
|
||||
**Avoids:** Stale off-market listings (hourly delta sync with `ModificationTimestamp` filter); IDX attribution violations (treated as acceptance criteria, not polish).
|
||||
**Research flag:** WFRMLS RESO API vendor enrollment takes 2-4 weeks — start this process immediately, in parallel with Phase 1. Do not block Phase 2 on this; build with mock data while approval is pending.
|
||||
|
||||
### Phase 3: Agent Portal Shell
|
||||
**Rationale:** Client management and the agent dashboard are prerequisites for the document workflow. These are straightforward CRUD operations with standard patterns.
|
||||
**Delivers:** Agent dashboard (skeleton), client list + create/edit, agent login page polish, navigation shell.
|
||||
**Addresses:** Client management (P1), document status dashboard (P1).
|
||||
**Avoids:** No pitfalls specific to this phase — standard CRUD patterns.
|
||||
**Research flag:** None needed — well-documented patterns.
|
||||
|
||||
### Phase 4: PDF Ingest and Storage
|
||||
**Rationale:** Must exist before field mapping UI can be built. The storage abstraction and PDF parsing pipeline are the lowest layer of the document workflow.
|
||||
**Delivers:** PDF upload (manual agent upload — no scraping), Vercel Blob storage pipeline (original/prepared/signed versions), pdfjs-dist AcroForm field extraction, document create + detail pages.
|
||||
**Addresses:** PDF upload + rendering (P1), signed document storage (P1).
|
||||
**Avoids:** Local filesystem storage (serverless ephemeral filesystem); utahrealestate.com scraping (manual upload only, no credentials stored, no headless browser automation in core app); PDF coordinate detection tested with actual Utah forms.
|
||||
**Research flag:** May need deeper research on pdfjs-dist Node.js legacy build for server-side parsing — uses `pdfjs-dist/legacy/build/pdf.mjs` without a worker, which is distinct from the browser build.
|
||||
|
||||
### Phase 5: PDF Fill and Field Mapping
|
||||
**Rationale:** Depends on Phase 4 (storage + parse infrastructure). The field mapper UI and the fill API are the agent's core document preparation workflow.
|
||||
**Delivers:** PDFFieldMapper.tsx (drag-to-place signature zones on PDF canvas), coordinate conversion from viewport space to PDF user space with unit tests, @pdfme/pdf-lib fill API (text fields, font embedding before flatten), document editor form.
|
||||
**Addresses:** Signature field placement UI (P1), agent-fills-then-client-signs workflow (differentiator).
|
||||
**Avoids:** PDF coordinate mismatch (unit-tested against actual Utah purchase agreement form before UI ships); font flattening failure (fonts embedded explicitly; tested in production serverless environment); heuristic-only detection (manual placement fallback is the primary flow; auto-detect is an enhancement).
|
||||
**Research flag:** Research the specific pdfjs-dist `viewport.convertToPdfPoint()` API and pdf-lib `StandardFonts` embedding before implementation — these are narrow but critical APIs.
|
||||
|
||||
### Phase 6: Signing Flow — End to End
|
||||
**Rationale:** The highest-complexity phase; depends on all previous phases. Contains all the legally critical components. Should be built as a complete vertical slice in one phase to ensure the audit trail is woven through from start to finish.
|
||||
**Delivers:** JWT token generation (HMAC-SHA256, 72-hour TTL, one-time enforcement with `used_at` DB column), resend email delivery, `/sign/[token]` public signing page, SignatureCanvas.tsx (mobile-first, touch-action:none, iOS Safari + Android Chrome tested on physical devices), @pdfme/pdf-lib signature PNG embed, SHA-256 hash of final signed PDF, complete SignatureAuditLog (6 ceremony events with server-side timestamps), one-time token invalidation with race-condition-safe DB transaction, "already signed" page for expired/used tokens.
|
||||
**Addresses:** Email delivery (P1), client signing page (P1), canvas capture (P1), audit trail (P1), signed document storage (P1), tamper-evident hash (P1).
|
||||
**Avoids:** Replayable signing tokens; incomplete audit trail; mobile canvas scroll-instead-of-sign; blank-canvas submission; font flattening blank text; unsigned PDF served publicly.
|
||||
**Research flag:** This phase warrants a `/gsd:research-phase` — the intersection of JWT one-time enforcement, PDF hash storage, ESIGN/UETA audit requirements, and mobile touch handling has enough edge cases that a focused spike before implementation reduces rework risk.
|
||||
|
||||
### Phase 7: Audit Trail, Status Tracking, and Download
|
||||
**Rationale:** Completes the agent-facing visibility layer. The underlying data is already being written in Phase 6; this phase surfaces it in the UI.
|
||||
**Delivers:** Document status tracking in agent dashboard (Draft/Sent/Viewed/Signed with last-activity timestamp), presigned Vercel Blob URLs for agent PDF download (5-minute TTL, authenticated route only), confirmation screen for client after signing, optional signed document email to client.
|
||||
**Addresses:** Agent dashboard status (P1 polish), signed document retrieval (P1), document view tracking (P2).
|
||||
**Avoids:** Signed PDFs accessible via guessable URL (all downloads gated behind authenticated presigned URLs).
|
||||
**Research flag:** None needed — well-documented patterns; the audit data infrastructure is already in place from Phase 6.
|
||||
|
||||
### Phase Ordering Rationale
|
||||
|
||||
- **Foundation first (Phase 1):** Auth, DB, and storage have zero optional dependencies. Every subsequent phase builds on these.
|
||||
- **Public site early (Phase 2):** Independent of the signing workflow; provides immediate value and allows WFRMLS API approval process to complete while portal phases are underway. The 2-4 week vendor enrollment timeline makes early start critical.
|
||||
- **Portal CRUD before PDF (Phase 3):** Clients and documents are the entities that the PDF pipeline operates on. Establishing the data model and basic CRUD before building the complex pipeline reduces schema churn.
|
||||
- **Storage before fill before sign (Phases 4-6):** Each phase adds one layer of the PDF pipeline. Building them in dependency order means each phase can ship independently and be tested in isolation.
|
||||
- **Audit/download last (Phase 7):** The underlying data is written by Phase 6. Phase 7 is surfacing and visibility only — no new data model needed.
|
||||
- **Scraping architecture decision made before Phase 4:** The decision to use manual upload (no utahrealestate.com scraping) must be explicit in Phase 4 so no scraping infrastructure is ever built.
|
||||
|
||||
### Research Flags
|
||||
|
||||
**Needs `/gsd:research-phase` during planning:**
|
||||
- **Phase 6 (Signing Flow):** Legal compliance intersection (ESIGN/UETA audit requirements + JWT one-time enforcement + PDF hash + mobile touch) has enough edge cases and gotchas that a pre-implementation research spike is warranted.
|
||||
- **Phase 2 (Listings):** WFRMLS vendor enrollment process and exact IDX compliance requirements (disclaimer text, 2024 NAR settlement fields) should be confirmed with WFRMLS directly before the listings UI is built — the requirements change with NAR policy updates.
|
||||
|
||||
**Standard patterns (skip research-phase):**
|
||||
- **Phase 1 (Foundation):** Next.js + Prisma + better-auth + Vercel Blob are all well-documented with official guides.
|
||||
- **Phase 3 (Portal Shell):** Standard Next.js CRUD patterns; no novel integration.
|
||||
- **Phase 7 (Audit/Status):** The data model is established; surfacing it is standard UI work.
|
||||
|
||||
## Confidence Assessment
|
||||
|
||||
| Area | Confidence | Notes |
|
||||
|------|------------|-------|
|
||||
| Stack | HIGH | All library versions verified via npm. next@15.5 vs 16 verified against official upgrade guide. pdf-lib unmaintained status confirmed (npm publish date). @pdfme/pdf-lib fork activity confirmed. better-auth + NextAuth merge confirmed via GitHub discussion. |
|
||||
| Features | HIGH | Feature list cross-referenced against multiple industry sources (DocuSign, HelloSign, SkySlope/Authentisign competitor analysis). ESIGN/UETA requirements sourced from legal and engineering references. Utah-specific requirements sourced from Utah DRE and WFRMLS. |
|
||||
| Architecture | HIGH | CVE-2025-29927 middleware bypass is a real, documented vulnerability (Vercel postmortem + ProjectDiscovery analysis). PDF coordinate system documented by multiple sources. Build order derived from dependency analysis, not assumption. |
|
||||
| Pitfalls | HIGH | Pitfalls sourced from court cases (e-signature audit trail failures), WFRMLS vendor FAQ (ToS prohibition on scraping), NAR IDX policy statement 7.58 (attribution requirements), and confirmed GitHub issues (pdf-lib coordinate bugs, font flattening). |
|
||||
|
||||
**Overall confidence:** HIGH across all research areas. Sources are primary (official docs, official policies, CVE disclosures) with consistent cross-referencing. The main area of uncertainty is not technical but logistical: WFRMLS vendor enrollment timeline (2-4 weeks, outside developer control).
|
||||
|
||||
### Gaps to Address
|
||||
|
||||
- **WFRMLS vendor enrollment timeline:** The RESO OData API requires a vendor contract, background check, and compliance review — 2-4 weeks. Start this process on day one. Build the listings page with mock data or a dev-environment token while waiting. Do not block Phase 2 on this.
|
||||
|
||||
- **Exact IDX disclaimer text:** WFRMLS provides required disclaimer text that must appear on every listing page. This text changes with NAR policy updates (2024 settlement changed required fields). Obtain the current required text directly from WFRMLS before the listings feature ships — do not copy from another agent's site.
|
||||
|
||||
- **Which Utah standard forms Teressa uses most frequently:** The manual upload workflow (replacing planned scraping) needs a curated set of base forms pre-loaded. Teressa should identify the 5-10 forms she uses in 90% of transactions so they can be manually uploaded and stored as reusable document templates in the app. This is a product/content decision, not a technical one.
|
||||
|
||||
- **Playwright deployment strategy (if forms import is added in v1.x):** Vercel serverless functions cannot run a full Playwright browser due to size limits. If the forms library import feature is added after v1, the Playwright scraping job must run on a separate service (Railway, Render, or a $5/month VPS with Browserless.io). This architecture decision must be made before any scraping code is written — and the ToS decision must be re-evaluated at that time.
|
||||
|
||||
- **SPF/DKIM/DMARC for teressacopelandhomes.com:** Signing link emails sent from an unverified sender domain will go to spam. DNS records must be configured before any signing link is sent to a real client. This is a DNS/infrastructure task, not a code task — it must be in the Phase 6 acceptance criteria.
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
|
||||
- [Next.js 15.5 Release Notes](https://nextjs.org/blog/next-15-5) — framework version and stable features
|
||||
- [Next.js 16 Upgrade Guide](https://nextjs.org/docs/app/guides/upgrading/version-16) — breaking changes confirming 15 is correct choice
|
||||
- [CVE-2025-29927 — Next.js Middleware Bypass (Vercel)](https://nextjs.org/blog/cve-2025-29927) — middleware auth bypass requiring defense-in-depth
|
||||
- [@pdfme/pdf-lib npm page](https://www.npmjs.com/package/@pdfme/pdf-lib) — v5.5.8, active maintenance confirmed
|
||||
- [pdfjs-dist npm](https://www.npmjs.com/package/pdfjs-dist) — v5.5.207 current
|
||||
- [signature_pad npm](https://www.npmjs.com/package/signature_pad) — v5.1.3
|
||||
- [better-auth npm](https://www.npmjs.com/package/better-auth) — v1.5.5; Auth.js merge confirmed
|
||||
- [UtahRealEstate.com Vendor Data Services](https://vendor.utahrealestate.com/) — RESO OData API and forms ToS confirmed
|
||||
- [WFRMLS RESO OData API Examples](https://www.reso.org/web-api-examples/mls/utah-mls/) — API field names and query syntax
|
||||
- [NAR IDX Policy Statement 7.58](https://www.nar.realtor/handbook-on-multiple-listing-policy/advertising-print-and-electronic-section-1-internet-data-exchange-idx-policy-policy-statement-7-58) — listing attribution requirements
|
||||
- [ESIGN/UETA Audit Trail Schema — Anvil Engineering](https://www.useanvil.com/blog/engineering/e-signature-audit-trail-schema-events-json-checklist/) — legal audit requirements
|
||||
- [pdf-lib Field Coordinates Issue — GitHub #602](https://github.com/Hopding/pdf-lib/issues/602) — coordinate API gap confirmed
|
||||
- [Utah DRE State-Approved Forms](https://commerce.utah.gov/realestate/real-estate/forms/state-approved/) — public domain forms source
|
||||
- [Vercel Blob documentation](https://vercel.com/docs/vercel-blob) — storage strategy confirmed
|
||||
- [Prisma 6.19.0 announcement](https://www.prisma.io/blog/announcing-prisma-6-19-0) — version confirmed
|
||||
- [Neon + Vercel integration](https://vercel.com/marketplace/neon) — serverless DB strategy
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
|
||||
- [NextAuth vs Clerk vs better-auth comparison (supastarter)](https://supastarter.dev/blog/better-auth-vs-nextauth-vs-clerk) — auth selection rationale (third-party analysis, but findings align with primary sources)
|
||||
- [JavaScript PDF Libraries Comparison 2025 (Nutrient)](https://www.nutrient.io/blog/javascript-pdf-libraries/) — PDF library selection (vendor-written but technically accurate)
|
||||
- [MLS Listing Data Freshness — MLSImport](https://mlsimport.com/fix-outdated-listings-on-your-wordpress-real-estate-site/) — delta sync approach
|
||||
- [Playwright vs Puppeteer 2025 (BrowserStack)](https://www.browserstack.com/guide/playwright-vs-puppeteer) — scraping tool selection
|
||||
|
||||
### Tertiary (LOW confidence — validate during implementation)
|
||||
|
||||
- [E-Signature UX Best Practices (various vendors)](https://www.esignglobal.com/blog/best-practices-embedded-signing-user-experience-ux) — UX recommendations sourced from vendor blogs; general patterns are sound but specific metrics need validation against real user behavior
|
||||
- WFRMLS exact required disclaimer text — must be obtained directly from WFRMLS; text changes with NAR policy and cannot be reliably sourced from third parties
|
||||
|
||||
---
|
||||
*Research completed: 2026-03-19*
|
||||
*Ready for roadmap: yes*
|
||||
Reference in New Issue
Block a user