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.
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
- [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<string,string> 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 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 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 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]: No Partially Signed enum added — partial state computed dynamically by counting usedAt IS NOT NULL tokens in Phase 16
- [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 19-02]: TemplatesListClient placed as sibling file to page.tsx — mirrors ClientsPageClient pattern for separation of concerns
- [Phase 19-02]: ConfirmDialog shown for all role removals (not just when fields > 0) — avoids async fetch for conditional UI; simpler and no UX flicker
- [Phase 20-apply-template-and-portal-nav]: documentTemplateId branch in POST /api/documents returns early so all existing paths unchanged; fresh crypto.randomUUID per field ensures snapshot independence
- [Phase 20-apply-template-and-portal-nav]: My Templates tab lazy-fetches /api/templates on first click via docTemplatesLoaded flag to avoid unnecessary network requests
- [Phase 20-apply-template-and-portal-nav]: Fields fetched on mount and aiPlacementKey change so hint stays current after AI auto-place; selectedFieldHint passed as optional prop to PreparePanel for backwards compatibility
- [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 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
- [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`