Files
2026-04-06 11:54:40 -06:00

9.2 KiB
Raw Permalink Blame History

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

// 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<SignatureFieldData[]>(),
  // Ordered list of signer roles with their assigned colors
  signerRoles: jsonb("signer_roles").$type<TemplateSignerRole[]>(),
  // Pre-filled static text hints for text/client-text fields
  textHints: jsonb("text_hints").$type<Record<string, string>>(),
  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: 1060 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.

-- 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 00000011)