Files
red/teressa-copeland-homes/.planning/research/STACK.md

217 lines
9.2 KiB
Markdown
Raw Normal View History

2026-04-06 11:54:40 -06:00
# 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<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.
```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 00000011)