--- gsd_state_version: 1.0 milestone: v1.1 milestone_name: Smart Document Preparation status: executing stopped_at: Completed 18-01-PLAN.md — documentTemplates schema and migration 0012 last_updated: "2026-04-06T18:15:59.504Z" last_activity: 2026-04-06 progress: total_phases: 20 completed_phases: 18 total_plans: 60 completed_plans: 58 percent: 82 --- # Project State ## Project Reference See: .planning/PROJECT.md (updated 2026-04-03) **Core value:** Teressa can prepare and send any real estate form to a client for signing in minutes, from her browser, without leaving her site. ## Current Position Phase: 18 (template-schema-and-crud-api) — EXECUTING Plan: 2 of 2 Status: Ready to execute Last activity: 2026-04-06 ## Note on v1.1 v1.1 (Smart Document Preparation) is complete except Phase 13 Plan 04 (E2E verification checkpoint). All feature code is implemented. Run `/gsd:plan-phase 13` to execute that final verification plan if needed. ## Note on v1.2 v1.2 (Multi-Signer and Deployment Hardening) roadmap is defined. Four phases planned: - Phase 14: Multi-Signer Schema — additive DB migration, enables Phases 15+16 - Phase 15: Multi-Signer Backend — token loop, signer-aware signing flow, atomic completion - Phase 16: Multi-Signer UI — PreparePanel signer list, FieldPlacer color-coding, send validation - Phase 17: Docker Deployment — three-stage Dockerfile, env_file secrets, health endpoint Start v1.2 after Phase 13 Plan 04 completes: `/gsd:plan-phase 14` ## Note on v1.3 v1.3 (Document Templates) roadmap created 2026-04-06. Three phases planned: - Phase 18: Template Schema and CRUD API — `document_templates` table (Drizzle migration), GET/POST/PATCH/DELETE API routes, soft-delete pattern - Phase 19: Template Editor UI — FieldPlacer `onPersist` abstraction, template editor page, AI auto-place via `/api/templates/[id]/ai-prepare`, signer role label support, save - Phase 20: Apply Template and Portal Nav — "Start from template" in AddDocumentModal, field snapshot with fresh UUIDs, role-to-email mapping, text hint quick-fill, Templates nav + list page Start v1.3 with: `/gsd:plan-phase 18` ## Accumulated Context ### Roadmap Evolution - Phase 11.1 inserted after Phase 11: Agent and Client Initials (URGENT) - v1.2 roadmap created 2026-04-03: Phases 14-17 (Multi-Signer + Docker Deployment) - v1.3 roadmap created 2026-04-06: Phases 18-20 (Document Templates) Progress: [████████████░░░] 82% (18/22 phases complete — counting v1.3 scope) ## Performance Metrics **Velocity:** - Total plans completed: 30 (v1.0 complete + Phase 8 complete) - Average duration: ~4 min/plan - Total execution time: ~2 hours **By Phase:** | Phase | Plans | Avg/Plan | |-------|-------|----------| | v1.0 phases 1-7 | 28 | ~4 min | **Recent Trend:** - Trend: Stable *Updated after each plan completion* | Phase 09-client-property-address P01 | 25 | 3 tasks | 7 files | | Phase 10-expanded-field-types-end-to-end P02 | 2 | 2 tasks | 2 files | | Phase 11-agent-saved-signature P01 | 8 | 2 tasks | 7 files | | Phase 11-agent-saved-signature P02 | 2 | 2 tasks | 2 files | | Phase 11-agent-saved-signature P03 | <1 | 1 task | 0 files | | Phase 11.1-agent-and-client-initials P01 | 2 | 2 tasks | 6 files | | Phase 11.1-agent-and-client-initials P02 | 4 | 2 tasks | 2 files | | Phase 12-filled-document-preview P01 | 5 | 2 tasks | 2 files | | Phase 12-filled-document-preview P02 | 25 | 3 tasks (2 auto + 1 human-verify) | 8 files | | Phase 12.1-per-field-text-editing-and-quick-fill P01 | 3 | 2 tasks | 4 files | | Phase 12.1-per-field-text-editing-and-quick-fill P02 | 5 | 2 auto tasks + 1 human-verify checkpoint | 2 files modified + 1 deleted | | Phase 13-ai-field-placement-and-pre-fill P01 | 2 | 2 tasks | 5 files | | Phase 13-ai-field-placement-and-pre-fill P03 | 2 | 2 tasks | 5 files | | Phase 14-multi-signer-schema P01 | 5 | 2 tasks | 3 files | | Phase 15 P01 | 2 | 3 tasks | 3 files | | Phase 15-multi-signer-backend P02 | 5 | 1 tasks | 1 files | | Phase 15-multi-signer-backend P03 | 2 | 2 tasks | 1 files | | Phase 16-multi-signer-ui P01 | 5 | 2 tasks | 6 files | | Phase 17-docker-deployment P01 | 2 | 2 tasks | 4 files | | Phase 17 P02 | 3 | 3 tasks | 6 files | | Phase 18-template-schema-and-crud-api P01 | 4 | 2 tasks | 2 files | ## Accumulated Context ### Decisions Decisions are logged in PROJECT.md Key Decisions table. Recent decisions affecting v1.1 work: - [v1.1 Research]: Use pdfjs-dist legacy build (already installed via react-pdf) for server-side PDF text extraction — no new dependency - [v1.1 Research]: Use manual `json_schema` response_format for OpenAI — do NOT use `zodResponseFormat` (broken with Zod v4, confirmed issues #1540, #1602, #1709) - [v1.1 Research]: Agent signature stored as base64 PNG TEXT column on users table (2-8KB) — no new file storage needed - [v1.1 Research]: Phase 8 must ship atomically (schema discriminant + signing page filter) before any new field type can be placed or sent - [08-01]: SignatureFieldType.type is optional on SignatureFieldData — v1.0 JSONB documents have no type; getFieldType() coalesces to 'client-signature' - [08-01]: isClientVisibleField() returns false only for 'agent-signature' — all other types including legacy documents are client-visible - [08-02]: Server-side filter in route.ts is the primary security boundary — client guards in SigningPageClient are defense-in-depth; direct API callers cannot see agent-signature coordinates - [08-02]: POST handler in route.ts intentionally untouched — signature embedding pipeline reads signatureFields from DB directly, not from client payload - [08-02]: Phase 8 ships atomically — 08-01 schema + 08-02 boundary enforcement active simultaneously; no intermediate state where type discriminant exists but filter is absent - [Phase 09-client-property-address]: TextFillForm initialData prop pattern: seed rows from Record via buildInitialRows helper; pre-seeding done via prop not external controlled state - [Phase 09-client-property-address]: Empty string from FormData coerced to NULL via || null before DB write — blank optional fields never store empty string in postgres - [Phase 10-expanded-field-types-end-to-end]: date field signing date captured server-side (now hoisted before step 8) — not trusted from client payload - [Phase 10-expanded-field-types-end-to-end]: signableFields filter limits POST signaturesWithCoords to client-signature and initials only — eliminates 500 on mixed-field documents - [Phase 10-expanded-field-types-end-to-end]: all field box backgrounds transparent in preparePdf — omit color param from drawRectangle = no fill; underlying PDF content always visible - [Phase 10-expanded-field-types-end-to-end]: checkbox draws X lines only (no rectangle); FieldPlacer hides resize handles for checkbox (fixed 24x24pt, non-resizable) - [Phase 10-expanded-field-types-end-to-end]: date stamp at sign time draws text directly — no white overwrite rectangle needed since no placeholder was drawn at prepare time - [Phase 11-01]: Migration auto-named 0008_windy_cloak.sql by drizzle-kit; content is correct ALTER TABLE users ADD COLUMN agent_signature_data text - [Phase 11-01]: AgentSignaturePanel fully self-contained — local state, fetch/save within component, no external context needed - [Phase 11-01]: Agent Signature palette token is red #dc2626 to visually distinguish from client-facing field types - [Phase 11-02]: PDFImage embedded once before field loop and reused via drawImage in loop — embed-once-draw-many pattern - [Phase 11-02]: Session guard strengthened to !session?.user?.id in prepare route — required for TypeScript narrowing of session.user.id to string - [Phase 11-02]: 422 guard fires only when hasAgentSigFields && !agentSignatureData — documents with no agent-sig fields prepare normally - [Phase 11-03]: No code changes in Plan 03 — all deliverables were complete in Plans 01 and 02; Plan 03 is pure human E2E verification - [Phase 11-03]: All four AGENT requirements (AGENT-01 through AGENT-04) verified by human in a single 5-step live test - [Phase 11.1-01]: Migration auto-named 0009_luxuriant_catseye.sql; agentInitialsData TEXT added to users table - [Phase 11.1-01]: AgentInitialsPanel clones AgentSignaturePanel with 80px canvas and /api/agent/initials endpoint - [Phase 11.1-01]: isClientVisibleField() now guards both agent-signature and agent-initials from client signing session - [Phase 11.1-01]: Orange #ea580c chosen for agent-initials FieldPlacer token — distinct from red agent-signature, visually grouped as agent-owned - [Phase 11.1-02]: agentInitialsData added as 6th optional param with default null — existing call sites compile without modification - [Phase 11.1-02]: 422 guard for missing agent-initials placed after existing agent-signature guard — parallel pattern for agent-owned field types - [Phase 11.1-02]: Single DB query fetches both agentSignatureData and agentInitialsData via columns selector — one round-trip - [Phase 11.1-03]: No code changes in Plan 03 — all deliverables were complete in Plans 01 and 02; Plan 03 is pure human E2E verification - [Phase 11.1-03]: All four INIT requirements (INIT-01 through INIT-04) verified by human in a single 5-step live test - [Phase 12-filled-document-preview]: Preview route uses _preview_{timestamp}.pdf versioned path — never overwrites _prepared.pdf - [Phase 12-filled-document-preview]: try/finally ensures temp preview file deleted even if readFile throws - [Phase 12-filled-document-preview]: PreviewModal configures pdfjs.GlobalWorkerOptions.workerSrc independently — not inherited from PdfViewer module - [Phase 12-filled-document-preview]: previewToken state lifted to DocumentPageClient — PreparePanel and FieldPlacer are siblings in server component, cannot share state directly - [Phase 12-filled-document-preview]: DocumentPageClient created as minimal client wrapper — holds previewToken state, passes handleFieldsChanged to FieldPlacer chain, passes previewToken+setter to PreparePanel - [Phase 12-02]: PreviewModal uses ReactDOM.createPortal to document.body — escapes sticky sidebar stacking context; z-index 9999 with body scroll lock via useEffect - [Phase 12-02]: Text fill values drawn at placed field box coordinates (field.x+4, field.y+4) — sorted by page/y asc; font 6-11pt; fieldConsumedKeys prevents Strategy B double-render - [Phase 12-02]: Text fill UX redesign (per-field click-to-edit, quick-fill suggestions) deferred to Phase 12.1 — known gap, not a correctness blocker; PREV-01 complete - [Phase Phase 12.1-01]: preparePdf text fill is now keyed by SignatureFieldData.id (UUID) — direct lookup replaces positional sort; AcroForm Strategy A and Strategy B top-of-page fallback both removed - [Phase Phase 12.1-01]: FieldPlacer click-to-select uses onClick (fires only on no-drag clicks due to MouseSensor distance:5 threshold) — onPointerDown move handler is preserved and unaffected - [Phase Phase 12.1-01]: data-no-move attribute on the inline input onPointerDown stops move handler activation — consistent with existing delete button and resize handle pattern - [Phase 12.1-02]: textFillData starts as {} in DocumentPageClient — NOT seeded from clientPropertyAddress; old label-keyed seeding ({ propertyAddress: clientPropertyAddress }) is removed; quick-fill makes it trivial to insert the value once a field is selected - [Phase 12.1-02]: handleFieldValueChange and handleQuickFill are separate useCallback functions that both call setPreviewToken(null) — satisfies TXTF-03 staleness reset on every text change - [Phase 12.1-02]: defaultEmail reused for Client Email quick-fill button — already a PreparePanel prop (used for recipients pre-fill); no new prop needed (per research Pitfall 3) - [Phase 12.1-02]: Human approved all 12 per-field text editing and quick-fill verification steps; TXTF-01, TXTF-02, TXTF-03 satisfied; Phase 12.1 complete - [Phase 13-ai-field-placement-and-pre-fill]: Manual json_schema response_format confirmed required — zodResponseFormat broken with Zod v4.3.6 (issues #1540, #1602, #1709) - [Phase 13-ai-field-placement-and-pre-fill]: Standard field sizes (checkbox=24x24, others=144x36pt) override AI widthPct/heightPct for consistency with FieldPlacer defaults - [Phase 13-ai-field-placement-and-pre-fill]: textFillData keyed by UUID assigned in route handler — matches Phase 12.1 design where DocumentPageClient uses field.id as key - [Phase 13-ai-field-placement-and-pre-fill]: GlobalWorkerOptions.workerSrc='' set at module level in extract-text.ts for Node.js fake-worker mode; client components set workerSrc independently - [Phase 13-02]: OPENAI_API_KEY guard in ai-prepare route returns 503 — distinguishes config error from 500 server error; more actionable for agent - [Phase 13-02]: try/catch wraps extractPdfText + classifyFieldsWithAI together — single error boundary returns 500 with error message string - [Phase 13-02]: ai-prepare route does NOT change document status — stays Draft so agent can review and adjust AI-placed fields before prepare/send - [Phase 13-02]: Path traversal guard placed before AI calls — fail fast without expensive PDF extraction on invalid paths - [Phase 13-03]: aiPlacementKey integer incremented via setAiPlacementKey(k => k + 1) — functional update prevents stale closure issues - [Phase 13-03]: textFillData is MERGED not replaced — preserves manually typed values while AI values take precedence - [Phase 13-03]: AI button uses violet bg-violet-600 to visually distinguish from gray Preview and blue Prepare-and-Send - [Phase 14-multi-signer-schema]: documents.signers JSONB shape is { email, color }[] so Phase 16 can retrieve consistent per-signer colors from DB without recalculating - [Phase 14-multi-signer-schema]: completionTriggeredAt nullable TIMESTAMP used as atomic completion guard — Phase 14 adds column; Phase 15 owns write logic - [Phase 14-multi-signer-schema]: No Partially Signed enum added — partial state computed dynamically by counting usedAt IS NOT NULL tokens in Phase 16 - [Phase 15]: signer-download token is 72h with no DB record — same pattern as agent-download, longer TTL for signer latency - [Phase 15]: signerEmail stored in DB only (not JWT payload) — keeps token minimal, consistent with D-03 - [Phase 15-multi-signer-backend]: Promise.all fail-fast for multi-signer send: one email failure rolls back entire send, agent retries — consistent with legacy single-signer behavior - [Phase 15-multi-signer-backend]: APP_BASE_URL replaces NEXT_PUBLIC_BASE_URL for signing URLs — server-side env var correct for API routes - [Phase 15-03]: GET filter uses tokenRow.signerEmail — null = legacy returns all isClientVisibleField fields (D-04, D-05) - [Phase 15-03]: POST accumulate: signedFilePath as working PDF — each signer reads latest, writes JTI-keyed partial (D-10) - [Phase 15-03]: completionTriggeredAt atomic guard ensures only one concurrent handler sets status=Signed and sends completion emails (D-07, D-08) - [Phase 16-multi-signer-ui]: DocumentPageClient owns signers and unassignedFieldIds state as single source of truth, threaded to PreparePanel and FieldPlacer chain - [Phase 16-multi-signer-ui]: Optional prop threading pattern with defaults at FieldPlacer leaf — backwards-compatible, Wave 2 wave plans consume props without breaking existing behavior - [Phase 17-docker-deployment]: output: 'standalone' in next.config.ts — required for Dockerfile three-stage build to produce self-contained server.js - [Phase 17-docker-deployment]: postgres pool max: 5 — Neon free tier allows 10 connections; 5 leaves headroom for migrations - [Phase 17-docker-deployment]: @vercel/blob removed — confirmed not imported anywhere in src/; dead dependency - [Phase 17]: node:20-slim (Debian) not Alpine — @napi-rs/canvas requires glibc, Alpine musl incompatible - [Phase 17]: seeds/ copied into runner stage at /app/seeds — runtime dependency for form library import feature - [Phase 17]: platform=linux/amd64 on all 3 Dockerfile FROM lines — home server is x86_64 - [Phase 18-template-schema-and-crud-api]: signatureFields nullable JSONB: template starts empty; Phase 19 fills it via PATCH - [Phase 18-template-schema-and-crud-api]: formTemplateId FK has no onDelete cascade: archived templates remain even if form is removed - [Phase 18-template-schema-and-crud-api]: archivedAt nullable timestamp: NULL = active, soft-delete only ### v1.2 Pre-decisions (from research) - [v1.2 Research]: Schema-first approach — Phase 14 schema deploys before any multi-signer app code to avoid hard-to-unwind bugs - [v1.2 Research]: signerEmail on signingTokens is nullable (TEXT) — legacy tokens have no signer; backfill via JOIN to client email in migration - [v1.2 Research]: completionTriggeredAt guard column uses UPDATE WHERE IS NULL RETURNING pattern — same as existing token claim; zero rows returned means another handler won - [v1.2 Research]: Per-signer PDF accumulation via pg_advisory_xact_lock(hashtext(documentId)) — existing embedSignatureInPdf() unchanged - [v1.2 Research]: NEXT_PUBLIC_BASE_URL renamed to APP_BASE_URL before Docker image build — prevents localhost being baked into signing link URLs - [v1.2 Research]: node:20-slim (Debian) required for @napi-rs/canvas glibc compatibility — do NOT use Alpine (musl libc incompatible) - [v1.2 Research]: Neon connection pool set to max:5 — prevents connection exhaustion on free tier - [v1.2 Research]: @vercel/blob is a dead dependency — remove before Phase 17 to prevent accidental use in Docker deployment ### v1.3 Pre-decisions (from research) - [v1.3 Research]: `document_templates` is a separate table from `form_templates` — form_templates is read-only seeded catalog; document_templates is agent-authored field layouts; FK from new to old - [v1.3 Research]: Template fields carry signer role labels ("Buyer", "Seller") in the signerEmail slot — the apply operation enforces a complete role-to-email map before DB INSERT; email format validation must not run during template editing - [v1.3 Research]: Field ID collision prevention is mandatory — stamp fresh crypto.randomUUID() on every field at apply time; template field IDs must never appear in documents - [v1.3 Research]: `hint` vs `textFillData` distinction — hint is a placeholder label stored in template; textFillData holds values burned into PDFs; never store hints in textFillData - [v1.3 Research]: Soft-delete only (`archivedAt` column) — no hard deletes; list API filters `archivedAt IS NULL`; documents.formTemplateId is lineage metadata only, no ON DELETE CASCADE - [v1.3 Research]: FieldPlacer `onPersist` callback is a non-breaking addition — existing document consumers pass no prop, behavior unchanged; template editor passes the callback to save to document_templates - [v1.3 Research]: `updatedAt` must be set explicitly in update queries — no DB trigger; follows existing pattern in all current tables - [v1.3 Research]: Single non-agent signer role auto-maps to assigned client email — reduces friction for solo-agent single-signer templates; agent can override ### Pending Todos None yet. ### Blockers/Concerns - [Phase 12 - CARRY FORWARD]: Deployment target confirmed as Docker (Phase 17 addresses this) — write-to-disk preview pattern is valid for self-hosted container - [Phase 13]: AI coordinate accuracy on real Utah forms is untested — integration test with full 20-page Utah REPC required before Phase 13 ships - [Phase 19 - PRE-DECISION NEEDED]: Confirm whether FieldPlacer has any `z.string().email()` or similar validation on `signerEmail` before Phase 19 begins — if so, introduce `mode: "template" | "document"` prop alongside `onPersist` ## Session Continuity Last session: 2026-04-06T18:15:59.498Z Stopped at: Completed 18-01-PLAN.md — documentTemplates schema and migration 0012 Resume file: None