docs: complete project research
This commit is contained in:
@@ -1,41 +1,39 @@
|
||||
# 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)
|
||||
**Researched:** 2026-03-21
|
||||
**Confidence:** HIGH (versions verified via npm registry; integration issues verified via official GitHub issues)
|
||||
**Scope:** v1.1 additions only — OpenAI integration, expanded field types, agent signature storage, filled preview
|
||||
|
||||
---
|
||||
|
||||
## Recommended Stack
|
||||
## Existing Stack (Do Not Re-research)
|
||||
|
||||
### Core Technologies
|
||||
Already validated and in `package.json`. Do not change these.
|
||||
|
||||
| Technology | Version in package.json | Role |
|
||||
|------------|------------------------|------|
|
||||
| Next.js | 16.2.0 | Full-stack framework |
|
||||
| React | 19.2.4 | UI |
|
||||
| `@cantoo/pdf-lib` | ^2.6.3 | PDF modification (server-side) |
|
||||
| `react-pdf` | ^10.4.1 | In-browser PDF rendering |
|
||||
| `signature_pad` | ^5.1.3 | Canvas signature drawing |
|
||||
| `zod` | ^4.3.6 | Schema validation |
|
||||
| `@vercel/blob` | ^2.3.1 | File storage |
|
||||
| Drizzle ORM + `postgres` | ^0.45.1 / ^3.4.8 | Database |
|
||||
| Auth.js (next-auth) | 5.0.0-beta.30 | Authentication |
|
||||
|
||||
---
|
||||
|
||||
## New Stack Additions for v1.1
|
||||
|
||||
### Core New Dependency: OpenAI API
|
||||
|
||||
| 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 |
|
||||
| `openai` | ^6.32.0 | OpenAI API client for GPT calls | Official SDK, current latest, TypeScript-native. Provides `client.chat.completions.create()` for structured JSON output via manual `json_schema` response format. Required for AI field placement and pre-fill. |
|
||||
|
||||
**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
|
||||
**No other new core dependencies are needed.** The remaining v1.1 features extend capabilities already in `@cantoo/pdf-lib`, `signature_pad`, and `react-pdf`.
|
||||
|
||||
---
|
||||
|
||||
@@ -43,384 +41,237 @@ Three separate libraries serve three distinct roles. You need all three.
|
||||
|
||||
| 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 |
|
||||
| `unpdf` | ^1.4.0 | Server-side PDF text extraction | Use in the AI pipeline API route to extract raw text from PDF pages before sending to OpenAI. Serverless-compatible, wraps PDF.js v5, works in Next.js API routes without native bindings. More reliable in serverless than `pdfjs-dist` directly. |
|
||||
|
||||
No other new supporting libraries needed. See "What NOT to Add" below.
|
||||
|
||||
---
|
||||
|
||||
### 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 |
|
||||
No new dev tooling required for v1.1 features.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
# New dependencies for v1.1
|
||||
npm install openai unpdf
|
||||
```
|
||||
|
||||
That is the full installation delta for v1.1.
|
||||
|
||||
---
|
||||
|
||||
## Feature-by-Feature Integration Notes
|
||||
|
||||
### Feature 1: OpenAI PDF Analysis + Field Placement
|
||||
|
||||
**Flow:**
|
||||
1. API route receives document ID
|
||||
2. Fetch PDF bytes from Vercel Blob (`@vercel/blob` — already installed)
|
||||
3. Extract text per page using `unpdf`: `getDocumentProxy()` + `extractText()`
|
||||
4. Call OpenAI `gpt-4o-mini` with extracted text + a manually defined JSON schema
|
||||
5. Parse structured response: array of `{ fieldType, label, pageNumber, x, y, width, height, suggestedValue }`
|
||||
6. Save placement records to DB via Drizzle ORM
|
||||
|
||||
**Why `gpt-4o-mini` (not `gpt-4o`):** Sufficient for structured field extraction on real estate forms. Significantly cheaper. The task is extraction from known document templates — not complex reasoning.
|
||||
|
||||
**Why manual JSON schema (not `zodResponseFormat`):** The project uses `zod` v4.3.6. The `zodResponseFormat` helper in `openai/helpers/zod` uses vendored `zod-to-json-schema` that still expects `ZodFirstPartyTypeKind` — removed in Zod v4. This is a confirmed open bug as of late 2025. Using `zodResponseFormat` with Zod v4 throws runtime exceptions. Use `response_format: { type: "json_schema", json_schema: { name: "...", strict: true, schema: { ... } } }` directly with plain TypeScript types instead.
|
||||
|
||||
```typescript
|
||||
// CORRECT for Zod v4 project — use manual JSON schema, not zodResponseFormat
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
response_format: {
|
||||
type: "json_schema",
|
||||
json_schema: {
|
||||
name: "field_placements",
|
||||
strict: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fields: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fieldType: { type: "string", enum: ["text", "checkbox", "initials", "date", "signature"] },
|
||||
label: { type: "string" },
|
||||
pageNumber: { type: "number" },
|
||||
x: { type: "number" },
|
||||
y: { type: "number" },
|
||||
width: { type: "number" },
|
||||
height: { type: "number" },
|
||||
suggestedValue: { type: "string" }
|
||||
},
|
||||
required: ["fieldType", "label", "pageNumber", "x", "y", "width", "height", "suggestedValue"],
|
||||
additionalProperties: false
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ["fields"],
|
||||
additionalProperties: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const result = JSON.parse(response.choices[0].message.content!);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Feature 2: Expanded Field Types in @cantoo/pdf-lib
|
||||
|
||||
**No new library needed.** `@cantoo/pdf-lib` v2.6.3 already supports all required field types natively:
|
||||
|
||||
| Field Type | @cantoo/pdf-lib API |
|
||||
|------------|---------------------|
|
||||
| Text | `form.createTextField(name)` → `.addToPage(page, options)` → `.setText(value)` |
|
||||
| Checkbox | `form.createCheckBox(name)` → `.addToPage(page, options)` → `.check()` / `.uncheck()` |
|
||||
| Initials | No dedicated type — use `createTextField` with width/height appropriate for initials |
|
||||
| Date | No dedicated type — use `createTextField`, constrain value format in application logic |
|
||||
| Agent Signature | Use `page.drawImage(embeddedPng, { x, y, width, height })` — see Feature 3 |
|
||||
|
||||
**Key pattern for checkboxes:**
|
||||
```typescript
|
||||
const checkBox = form.createCheckBox('fieldName')
|
||||
checkBox.addToPage(page, { x, y, width: 15, height: 15, borderWidth: 1 })
|
||||
if (shouldBeChecked) checkBox.check()
|
||||
```
|
||||
|
||||
**Coordinate system note:** `@cantoo/pdf-lib` uses PDF coordinate space where y=0 is the bottom of the page. If field positions come from `unpdf` / PDF.js (which uses y=0 at top), you must transform: `pdfY = pageHeight - sourceY - fieldHeight`.
|
||||
|
||||
---
|
||||
|
||||
### Feature 3: Agent Signature Storage
|
||||
|
||||
**No new library needed.** The project already has `signature_pad` v5.1.3, `@vercel/blob`, and Drizzle ORM.
|
||||
|
||||
**Architecture:**
|
||||
1. Agent draws signature in browser using `signature_pad` (already installed)
|
||||
2. Call `signaturePad.toDataURL('image/png')` to get base64 PNG
|
||||
3. POST to API route; server converts base64 → `Uint8Array` → uploads to Vercel Blob at a stable path (e.g., `/agents/{agentId}/signature.png`)
|
||||
4. Save blob URL to agent record in DB (add `signatureImageUrl` column to `Agent`/`User` table via Drizzle migration)
|
||||
5. On "apply agent signature": server fetches blob URL, embeds PNG into PDF using `@cantoo/pdf-lib`
|
||||
|
||||
**`signature_pad` v5 in React — use `useRef` on a `<canvas>` element directly:**
|
||||
|
||||
```typescript
|
||||
import SignaturePad from 'signature_pad'
|
||||
import { useRef, useEffect } from 'react'
|
||||
|
||||
export function SignatureDrawer() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const padRef = useRef<SignaturePad | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
padRef.current = new SignaturePad(canvasRef.current)
|
||||
}
|
||||
return () => padRef.current?.off()
|
||||
}, [])
|
||||
|
||||
const save = () => {
|
||||
const dataUrl = padRef.current?.toDataURL('image/png')
|
||||
// POST dataUrl to /api/agent/signature
|
||||
}
|
||||
|
||||
return <canvas ref={canvasRef} width={400} height={150} />
|
||||
}
|
||||
```
|
||||
|
||||
**Do NOT add `react-signature-canvas`.** It wraps `signature_pad` at v1.1.0-alpha.2 (alpha status) and the project already has `signature_pad` directly. Use the raw library with a `useRef`.
|
||||
|
||||
**Embedding the saved signature into PDF:**
|
||||
```typescript
|
||||
const sigBytes = await fetch(agentSignatureBlobUrl).then(r => r.arrayBuffer())
|
||||
const sigImage = await pdfDoc.embedPng(new Uint8Array(sigBytes))
|
||||
const dims = sigImage.scaleToFit(fieldWidth, fieldHeight)
|
||||
page.drawImage(sigImage, { x: fieldX, y: fieldY, width: dims.width, height: dims.height })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Feature 4: Filled Document Preview
|
||||
|
||||
**No new library needed.** `react-pdf` v10.4.1 is already installed and supports rendering a PDF from an `ArrayBuffer` directly.
|
||||
|
||||
**Architecture:**
|
||||
1. Server Action: load original PDF from Vercel Blob, apply all field values (text, checkboxes, embedded signature image) using `@cantoo/pdf-lib`, return `pdfDoc.save()` bytes
|
||||
2. API route returns the bytes as `application/pdf`; client receives as `ArrayBuffer`
|
||||
3. Pass `ArrayBuffer` directly to `react-pdf`'s `<Document file={arrayBuffer}>` — no upload required
|
||||
|
||||
**Known issue with react-pdf v7+:** `ArrayBuffer` becomes detached after first use. Always copy:
|
||||
```typescript
|
||||
const safeCopy = (buf: ArrayBuffer) => {
|
||||
const copy = new ArrayBuffer(buf.byteLength)
|
||||
new Uint8Array(copy).set(new Uint8Array(buf))
|
||||
return copy
|
||||
}
|
||||
<Document file={safeCopy(previewBuffer)}>
|
||||
```
|
||||
|
||||
**react-pdf renders the flattened PDF accurately** — all filled text fields, checked checkboxes, and embedded signature images will appear correctly because they are baked into the PDF bytes by `@cantoo/pdf-lib` before rendering.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
| `unpdf` for text extraction | `pdfjs-dist` directly in Node API route | `pdfjs-dist` v5 uses `Promise.withResolvers` requiring Node 22+; the project targets Node 20 LTS. `unpdf` ships a polyfilled serverless build that handles this. |
|
||||
| `unpdf` for text extraction | `pdf-parse` | `pdf-parse` is unmaintained (last publish 2019). `unpdf` is the community-recommended successor. |
|
||||
| Manual JSON schema for OpenAI | `zodResponseFormat` helper | Broken with Zod v4 — open bug in `openai-node` as of Nov 2025. Manual schema avoids the dependency entirely. |
|
||||
| `gpt-4o-mini` | `gpt-4o` | Real estate form field extraction is a structured extraction task on templated documents. `gpt-4o-mini` is sufficient and ~15x cheaper. Upgrade to `gpt-4o` only if accuracy on unusual forms is unacceptable. |
|
||||
| `page.drawImage()` for agent signature | `PDFSignature` AcroForm field | `@cantoo/pdf-lib` has no `createSignature()` API — `PDFSignature` only reads existing signature fields and provides no image embedding. The correct approach is `embedPng()` + `drawImage()` at the field coordinates. |
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Use
|
||||
## What NOT to Add
|
||||
|
||||
| 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 |
|
||||
| `zodResponseFormat` from `openai/helpers/zod` | Broken at runtime with Zod v4.x (throws exceptions). Open bug, no fix merged as of 2026-03-21. | Plain `response_format: { type: "json_schema", ... }` with hand-written schema |
|
||||
| `react-signature-canvas` | Alpha version (1.1.0-alpha.2); project already has `signature_pad` v5 directly — the wrapper adds nothing | `signature_pad` + `useRef<HTMLCanvasElement>` directly |
|
||||
| `@signpdf/placeholder-pdf-lib` | For cryptographic PKCS#7 digital signatures (DocuSign-style). This project needs visual e-signatures (image embedded in PDF), not cryptographic signing. | `@cantoo/pdf-lib` `embedPng()` + `drawImage()` |
|
||||
| `pdf2json` | Extracts spatial text data; useful for arbitrary document analysis. Overkill here — we only need raw text content to feed OpenAI. | `unpdf` |
|
||||
| `langchain` / Vercel AI SDK | Heavy abstractions for the simple use case of one structured extraction call per document. Adds bundle size and abstraction layers with no benefit here. | `openai` SDK directly |
|
||||
| A separate image processing library (`sharp`, `jimp`) | Not needed — signature PNGs from `signature_pad.toDataURL()` are already correctly sized canvas exports. `@cantoo/pdf-lib` handles embedding without pre-processing. | N/A |
|
||||
|
||||
---
|
||||
|
||||
## PDF Processing Strategy
|
||||
## Version Compatibility
|
||||
|
||||
### 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?
|
||||
}
|
||||
```
|
||||
| Package | Compatible With | Notes |
|
||||
|---------|-----------------|-------|
|
||||
| `openai@6.32.0` | `zod@4.x` (manual schema only) | Do NOT use `zodResponseFormat` helper — use raw `json_schema` response_format. The helper is broken with Zod v4. |
|
||||
| `openai@6.32.0` | Node.js 20+ | Requires Node 20 LTS or later. Next.js 16.2 on Vercel uses Node 20 by default. |
|
||||
| `unpdf@1.4.0` | Node.js 18+ | Bundled PDF.js v5.2.133 with polyfills for `Promise.withResolvers`. Works on Node 20. |
|
||||
| `@cantoo/pdf-lib@2.6.3` | `react-pdf@10.4.1` | These do not interact at runtime — `@cantoo/pdf-lib` runs server-side, `react-pdf` runs client-side. No conflict. |
|
||||
| `signature_pad@5.1.3` | React 19 | Use as a plain class instantiated in `useEffect` with a `useRef<HTMLCanvasElement>`. No React wrapper needed. |
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
- [openai npm page](https://www.npmjs.com/package/openai) — v6.32.0 confirmed, Node 20 requirement — HIGH confidence
|
||||
- [OpenAI Structured Outputs docs](https://platform.openai.com/docs/guides/structured-outputs) — manual json_schema format confirmed — HIGH confidence
|
||||
- [openai-node Issue #1540](https://github.com/openai/openai-node/issues/1540) — zodResponseFormat broken with Zod v4 — HIGH confidence
|
||||
- [openai-node Issue #1602](https://github.com/openai/openai-node/issues/1602) — zodTextFormat broken with Zod 4 — HIGH confidence
|
||||
- [openai-node Issue #1709](https://github.com/openai/openai-node/issues/1709) — Zod 4.1.13+ discriminated union break — HIGH confidence
|
||||
- [@cantoo/pdf-lib npm page](https://www.npmjs.com/package/@cantoo/pdf-lib) — v2.6.3, field types confirmed — HIGH confidence
|
||||
- [pdf-lib.js.org PDFForm docs](https://pdf-lib.js.org/docs/api/classes/pdfform) — createTextField, createCheckBox, drawImage APIs — HIGH confidence
|
||||
- [unpdf npm page](https://www.npmjs.com/package/unpdf) — v1.4.0, serverless PDF.js build, Node 20 compatible — HIGH confidence
|
||||
- [unpdf GitHub](https://github.com/unjs/unpdf) — extractText API confirmed — HIGH confidence
|
||||
- [react-pdf npm page](https://www.npmjs.com/package/react-pdf) — v10.4.1, ArrayBuffer file prop confirmed — HIGH confidence
|
||||
- [react-pdf ArrayBuffer detach issue #1657](https://github.com/wojtekmaj/react-pdf/issues/1657) — copy workaround confirmed — HIGH confidence
|
||||
- [signature_pad GitHub](https://github.com/szimek/signature_pad) — v5.1.3, toDataURL API — HIGH confidence
|
||||
- [pdf-lib image embedding JSFiddle](https://jsfiddle.net/Hopding/bcya43ju/5/) — embedPng/drawImage pattern — HIGH confidence
|
||||
|
||||
---
|
||||
|
||||
*Stack research for: Teressa Copeland Homes — real estate agent website + document signing*
|
||||
*Researched: 2026-03-19*
|
||||
*Stack research for: Teressa Copeland Homes — v1.1 Smart Document Preparation additions*
|
||||
*Researched: 2026-03-21*
|
||||
|
||||
Reference in New Issue
Block a user