5.9 KiB
Phase 18: Template Schema and CRUD API - Context
Gathered: 2026-04-06 Status: Ready for planning
## Phase BoundarySchema migration + CRUD API only. No UI, no template editor, no apply-to-document logic. Those are Phases 19 and 20.
Deliverables:
- Drizzle migration adding
document_templatestable GET /api/templates— list active templates (archivedAt IS NULL)POST /api/templates— create template (pick a form from the library)PATCH /api/templates/[id]— rename templateDELETE /api/templates/[id]— soft-delete (set archivedAt)
Schema Design
-
D-01: New table
document_templates— separate fromform_templates. Theform_templatestable is a read-only seeded catalog.document_templatesis agent-authored. -
D-02: Table columns:
document_templates: id TEXT PRIMARY KEY (crypto.randomUUID()) name TEXT NOT NULL formTemplateId TEXT NOT NULL REFERENCES form_templates(id) signatureFields JSONB NULL — SignatureFieldData[] with roles in signerEmail slot, hints in hint slot archivedAt TIMESTAMP NULL — soft-delete; NULL = active createdAt TIMESTAMP NOT NULL DEFAULT NOW() updatedAt TIMESTAMP NOT NULL DEFAULT NOW() -
D-03: NO separate
signerRolesortextHintscolumns. Both are already embedded per-field insignatureFields:- Role labels stored in
field.signerEmail(e.g. "Buyer", "Seller") - Text hints stored in
field.hint(already onSignatureFieldData) - No duplication, no sync issues.
- Role labels stored in
-
D-04:
signatureFieldsis nullable on creation (template starts empty, editor fills it in Phase 19). Default NULL. -
D-05:
updatedAtmust be set explicitly in every UPDATE query (no DB trigger). Follow existing pattern fromclientsandformTemplatestables. -
D-06: NO
ON DELETE CASCADEon theformTemplateIdFK. If a form is removed from the library, templates referencing it should remain (archivedAt pattern handles cleanup).
Soft-Delete
- D-07: Deletion sets
archivedAt = NOW(). Hard delete is never performed. - D-08: All list queries filter
WHERE archivedAt IS NULL. Archived templates are invisible to the agent. - D-09: Archived templates are NOT restored. If agent needs the template back, they create a new one.
API Structure
-
D-10: Routes at
/api/templates(top-level, matching/api/documentspattern):GET /api/templates— returns active templates with: id, name, formTemplateId, formName (joined), fieldCount (derived from signatureFields length), createdAt, updatedAtPOST /api/templates— body:{ name: string; formTemplateId: string }— creates with empty signatureFieldsPATCH /api/templates/[id]— body:{ name?: string; signatureFields?: SignatureFieldData[] }— partial updateDELETE /api/templates/[id]— sets archivedAt = NOW()
-
D-11: All routes auth-gated (agent session required). Same
auth()guard as all other portal API routes. -
D-12:
fieldCounton GET list is derived server-side:(signatureFields ?? []).length. Not stored in DB.
Migration
- D-13: Use existing Drizzle pattern: edit
schema.ts, rundrizzle-kit generate→drizzle/0012_*.sql, commit both. Next migration after0011_common_mystique.sql.
Claude's Discretion
- Exact migration filename (Drizzle generates automatically)
- Whether to include formTemplate name JOIN in the list query (recommended — avoids extra fetches in UI)
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Schema (existing patterns to follow)
teressa-copeland-homes/src/lib/db/schema.ts— existing tables;document_templatesgoes here; followclientstable pattern for updatedAtteressa-copeland-homes/drizzle/— existing migrations 0000–0011; new migration is 0012teressa-copeland-homes/drizzle.config.ts— migration config
Existing API patterns
teressa-copeland-homes/src/app/api/documents/route.ts— POST documents pattern to followteressa-copeland-homes/src/app/api/documents/[id]/route.ts— PATCH pattern to follow
Research
.planning/research/ARCHITECTURE.md— schema decision reasoning, build order.planning/research/PITFALLS.md— soft-delete rationale, FK constraint notes
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
auth()from@/lib/auth— all existing API routes use this for session guard; follow same patterndbfrom@/lib/db— Drizzle client; import and use directlySignatureFieldDatainterface — already in schema.ts;signatureFieldscolumn types toSignatureFieldData[]formTemplatestable — JOIN target for getting form name in list query
Established Patterns
crypto.randomUUID()via$defaultFnfor IDs — all existing tables use this patterntimestamp("updated_at").defaultNow().notNull()— existing pattern; updated explicitly in every PATCHjsonb("column").$type<T>()— existing JSONB pattern (signatureFields, signers, contacts)text("foreign_key_id").notNull().references(() => otherTable.id)— FK pattern
Integration Points
formTemplatestable (existing) —document_templates.formTemplateIdFKs to this- Phase 19 will add the template editor UI that PATCHes
signatureFields - Phase 20 will add the apply-template endpoint (POST to create document from template)
</code_context>
## Specific IdeasfieldCountin GET /api/templates response:(t.signatureFields?.length ?? 0)— computed at query time, no extra DB column- PATCH route handles both rename (
name) AND field save (Phase 19 calls it withsignatureFields) — one route, two use cases
None — discussion stayed within phase scope.
Phase: 18-template-schema-and-crud-api Context gathered: 2026-04-06