# Technology Stack — Document Templates Feature **Project:** teressa-copeland-homes **Feature:** Document Templates (reusable PDF + pre-placed fields + signer role assignments) **Researched:** 2026-04-06 **Overall confidence:** HIGH — analysis is based on direct inspection of the live codebase --- ## Verdict: Zero New Packages Required The existing stack handles document templates entirely. This feature is a schema extension plus UI reuse — no new runtime dependencies are needed. --- ## Existing Stack (Validated — Do Not Re-research) | Layer | Technology | Version | |-------|-----------|---------| | Framework | Next.js App Router | 16.2.0 | | Language | TypeScript | ^5 | | ORM | Drizzle ORM | ^0.45.1 | | Database | PostgreSQL (local dev / Neon prod) | — | | PDF manipulation | @cantoo/pdf-lib | ^2.6.3 | | PDF rendering | react-pdf | ^10.4.1 | | Drag-drop fields | @dnd-kit/core + @dnd-kit/utilities | ^6.3.1 / ^3.2.2 | | AI field placement | openai (GPT-4.1) | ^6.32.0 | | Validation | zod | ^4.3.6 | | Auth | next-auth | 5.0.0-beta.30 | --- ## Schema Change: New `document_templates` Table The current `form_templates` table stores only raw PDFs (id, name, filename). It has no field data. A new table is needed — it must NOT be confused with `form_templates`, which is a forms library (source PDFs only). ### New table: `document_templates` ```typescript // src/lib/db/schema.ts addition export interface TemplateSignerRole { role: string; // e.g. "Seller", "Buyer", "Agent" color: string; // hex — matches signer color convention from DocumentSigner } export const documentTemplates = pgTable("document_templates", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), name: text("name").notNull(), // FK to form_templates — the source PDF this template was built from formTemplateId: text("form_template_id") .notNull() .references(() => formTemplates.id, { onDelete: "restrict" }), // Pre-placed fields — same SignatureFieldData[] shape used everywhere else // signerEmail on each field will hold a ROLE string here (e.g. "Seller"), // not a real email. Resolved to actual emails at apply-time. signatureFields: jsonb("signature_fields").$type(), // Ordered list of signer roles with their assigned colors signerRoles: jsonb("signer_roles").$type(), // Pre-filled static text hints for text/client-text fields textHints: jsonb("text_hints").$type>(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); ``` ### Why a new table, not extending `form_templates` `form_templates` is a seeded forms library (120+ Utah/CCIM PDF forms). It is read-only catalog data. Adding mutable agent-authored field data to it would mix concerns: catalog vs. prepared template. A clean FK relationship (`document_templates.formTemplateId → form_templates.id`) keeps them separated and allows one form to have multiple named templates (e.g. "Standard Listing Agreement", "Listing Agreement + Short Sale Addendum"). ### Why not extend `documents` `documents` is a per-client instance with a concrete `clientId`, real signer emails, and a lifecycle (Draft → Sent → Signed). Templates are agent-owned, have no client, and have no signing lifecycle. Mixing them would require nullable foreign keys and conditional logic throughout the signing flow. --- ## Apply-Template Operation: Pure Data Transform, No New Packages When an agent applies a template to a new document, the operation is: 1. Load `document_templates` row (fields, signerRoles, textHints) 2. Create a new `documents` row with `formTemplateId` copied from the template's source 3. Deep-clone `signatureFields` array; substitute role strings → real signer emails supplied by the agent at apply-time (e.g. "Seller" → "alice@example.com") 4. Copy `textHints` into `documents.textFillData` as the initial fill data 5. `documents.signers` is populated from the resolved email+color pairs This is entirely Drizzle + TypeScript. No transformation library needed. --- ## JSONB Performance: No Concern at This Scale **Field for concern:** `signatureFields` JSONB arrays in both `document_templates` and `documents`. Typical Utah real estate form: 10–60 fields. Maximum realistic: ~150 fields on a complex addendum stack. **PostgreSQL JSONB behavior:** JSONB is stored binary, fully indexed when needed. At < 200 fields per document and < 1000 total templates, there is no performance risk with unindexed JSONB. The existing `documents.signatureFields` already uses this pattern at production with no issues. **If GIN indexing were ever needed** (e.g. querying "all templates that have a field of type X"), `drizzle-kit` supports it via `index()`. Not needed for the MVP. --- ## Migration Path This is an additive migration — no existing tables are altered. ```sql -- Generated by: npx drizzle-kit generate CREATE TABLE "document_templates" ( "id" text PRIMARY KEY, "name" text NOT NULL, "form_template_id" text NOT NULL REFERENCES "form_templates"("id") ON DELETE RESTRICT, "signature_fields" jsonb, "signer_roles" jsonb, "text_hints" jsonb, "created_at" timestamp DEFAULT now() NOT NULL, "updated_at" timestamp DEFAULT now() NOT NULL ); ``` Follow existing migration pattern: add to schema.ts, run `npm run db:generate`, then `npm run db:migrate`. --- ## UI Reuse All FieldPlacer / DocumentPageClient / PreparePanel / PdfViewerWrapper components can be reused verbatim for the template editor. The template editor is functionally identical to the document prepare flow — same drag-drop, same AI auto-place endpoint — except: - No `clientId` context (no real signer emails yet; use role names as placeholder emails) - No "Send" action (templates are saved, not sent) - Signer panel shows roles ("Seller", "Buyer") not real email addresses This means the template editor is a new route (`/portal/templates/[templateId]/edit`) that instantiates the existing components with role-mode props, not a new component tree. --- ## Integration Points | Existing File | Change Needed | |---------------|--------------| | `src/lib/db/schema.ts` | Add `documentTemplates` table + `TemplateSignerRole` interface | | `src/app/api/documents/route.ts` | Accept optional `documentTemplateId` on POST; apply-template transform | | `src/app/portal/_components/AddDocumentModal.tsx` | Add "From template" tab alongside "From forms library" | | `drizzle/` | New migration file (auto-generated by drizzle-kit) | No changes needed to: signing flow, PDF preparation, email sending, audit events. --- ## Alternatives Considered | Option | Why Rejected | |--------|-------------| | Extend `form_templates` with field columns | Mixes catalog data with agent-authored data; blocks multi-template-per-form | | Store template fields inside `documents` with a `isTemplate` flag | Breaks document lifecycle invariants; complicates all document queries | | New npm package for template merge (e.g. json-merge-patch) | Not needed — deep clone + email substitution is trivial TypeScript | | Separate template file storage (copy PDF per template) | Wasteful; templates share the original `form_templates` PDF via FK | --- ## Confidence Assessment | Area | Confidence | Basis | |------|------------|-------| | Zero new packages | HIGH | Direct schema/code inspection; transform is pure TypeScript | | New table over extending existing | HIGH | `form_templates` is seeded catalog; concerns are clearly different | | JSONB performance | HIGH | Existing pattern already in production; field counts well within limits | | UI reuse via props | MEDIUM | Components not yet inspected for hardcoded assumptions about signer emails vs roles — confirm before implementation | | Migration is additive | HIGH | No existing column changes; FK is to read-only catalog table | --- ## Open Questions for Implementation Phase 1. **Role-string convention in `signerEmail` field:** The `SignatureFieldData.signerEmail` field currently holds real email addresses. Using it for role strings ("Seller") during template editing requires the FieldPlacer and PreparePanel components to handle both modes. Confirm these components have no email-format validation before proceeding. 2. **Template apply: partial field substitution.** If a template has 3 signer roles but the agent assigns only 2 real signers at apply-time, the behavior needs to be defined (error, or leave unassigned fields with no signer). 3. **`updatedAt` auto-update:** The current schema uses `.defaultNow()` for `updatedAt` but does not use a DB trigger for auto-update on mutation. Existing tables share this pattern — use the same convention (set `updatedAt` explicitly in update queries). --- ## Sources - Direct inspection: `/Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/db/schema.ts` - Direct inspection: `/Users/ccopeland/temp/red/teressa-copeland-homes/package.json` - Direct inspection: `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx` - Direct inspection: `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/documents/[id]/fields/route.ts` - Direct inspection: `/Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/` (migrations 0000–0011)