Files
red/.planning/research/PITFALLS.md

511 lines
49 KiB
Markdown
Raw Normal View History

2026-03-19 11:50:51 -06:00
# 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 24 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*