Skip to content

Policy Template Platform — Implementation Plan

Status: Planning
Scope: Immediate bug fixes → Phase 1 (seed catalog + import wizard) → Phase 2 (AI-assisted creation)
Related: PLATFORM_ADMIN.md · RBAC_Matrix.md


Diagnosis

Why tenant Policy Management never shows templates

The template and recommendation engines are fully built but the tenant-facing entry point was never wired up. Two disconnects cause the entire chain to fail silently:

Disconnect 1 — /policies/new bypasses TemplatePicker entirely

src/app/policies/new/page.tsx renders PolicyForm directly. The TemplatePicker component exists and works, but nothing ever calls it. The tenant sees only a blank form — no template suggestions, no recommendations panel, no indication that templates exist.

Disconnect 2 — createPolicy has no template fields

src/lib/services/policy-service.ts::createPolicy accepts title, category, department, purpose, policyStatement, and scope — none of which relate to templates. Even if a tenant selected a template, there is nowhere to store sourceTemplateId, sourceVersion, or sections[]. The policy would be created as a blank document with no link back to the template.

Downstream effect — tenant sees no templates anywhere

Because neither disconnect has been resolved, every tenant's /policies/new page shows a blank form. The recommendation engine (rule-engine.ts) is correct and functional; the rule created for Remote Work Security Policy is stored and active. The template won't surface until the TemplatePicker is wired in and the policy creation flow uses it.

Data model gap

Templates store content in sections[] (each section has title, body rich-text, isOptional, order). Tenant policies store content in flat fields: purpose (short text) and policyStatement (long text). These are structurally incompatible.

Resolution: When a policy is created from a template, populate sections[] on the policy document alongside the flat fields. The PolicyEditor already renders sections if present (confirmed by src/components/policies/PolicyEditor.tsx). Policies created from scratch continue to use the flat model.

Genkit status

genkit and genkit-cli are listed in package.json and in two dead scripts (genkit:dev, genkit:watch). No source files import either package — removal is safe and clean.


Part 0 — Immediate Fixes

These are blocking. Phase 1 cannot work until all four are resolved.

Fix 0.1 — Wire TemplatePicker as Step 0 of /policies/new

File: src/app/policies/new/page.tsx

Replace the current single-step page (renders PolicyForm immediately) with a two-step wizard:

  • Step 0 — Template Discovery: Render TemplatePicker. It calls /api/policies/recommend and shows matched templates ranked by weight. The user picks a template or clicks "Create from scratch" to skip.
  • Step 1 — Policy Form: Render PolicyForm, pre-populated with sections from the chosen template (or blank if skipped).

The step state is local to the page component: type Step = 'pick-template' | 'fill-form' and selectedTemplateId: string | null.

Fix 0.2 — Add template adoption fields to createPolicy

Files: src/app/api/policies/route.ts, src/lib/services/policy-service.ts

Extend the create schema and service signature:

// New optional fields in createSchema (route.ts)
sourceTemplateId: z.string().optional(),
sourceVersion:    z.number().int().optional(),
sections:         z.array(PolicySectionSchema).optional(),

When sourceTemplateId is supplied, createPolicy stores it alongside the standard fields. The policy document gains:

sourceTemplateId: "tmpl-abc123"
sourceVersion:    1
sections:         [ { id, title, body, isOptional, order } ... ]

Fix 0.3 — Pre-fill sections when adopting a template

File: src/app/policies/new/page.tsx (in the transition between Step 0 and Step 1)

After the tenant selects a template, fetch it: GET /api/platform-admin/policy-templates/:idbut this route is platform-admin only. Add a new tenant-accessible route:

GET /api/policies/templates/:id

Protected by withAuth (any authenticated tenant user), returns the template's sections[], title, category, and placeholders[]. Does not expose internal fields (authorId, changelog, etc.). This is the only cross-boundary read that is safe: published template content is public by design.

The pre-fill step substitutes known placeholder values: - {{COMPANY_NAME}} → tenant's companyName from Firestore - {{EFFECTIVE_DATE}} → today's date formatted - {{JURISDICTION}} → tenant's jurisdiction (if set)

Unknown placeholders remain as {{TOKEN}} and are shown to the user as editable fields.

Fix 0.4 — Fallback when no recommendations exist

File: src/components/policies/TemplatePicker.tsx and src/app/api/policies/recommend/route.ts

When getRecommendationsForTenant returns [], the current UI shows "No template recommendations for your profile" with only a "Create from Scratch" button. This is a dead end.

New behaviour: - Add query param: GET /api/policies/recommend?fallback=true - When fallback=true and recommendations are empty, the endpoint returns all published templates ordered by updatedAt descending, with isRequired: false, weight: 0, and reasonText: 'General template — no specific recommendation for your profile.' - TemplatePicker automatically adds ?fallback=true, so the tenant always sees something - The component distinguishes "matched" vs "fallback" items with a subtle UI indicator

A separate concern: the platform admin should be notified when a tenant's profile matches zero rules. This belongs in a future health dashboard, not in this plan.


Part 1 — Seed Catalog and Import Wizard

1.1 Canonical source file

New file: db/policy-templates-catalog.ts

This is the single source of truth for the pre-built template library. It exports a typed array of CatalogEntry objects where each entry bundles a complete template definition with its associated rules. The file is linked from PLATFORM_ADMIN.md so anyone editing sample content knows where to look.

export interface CatalogEntry {
  template: {
    title: string;
    category: PolicyCategory;
    sections: PolicySection[];     // full content with {{PLACEHOLDER}} tokens
    placeholders: string[];        // list of token names used
  };
  rules: Array<{
    industryCode: string | null;   // 2-digit NAICS, or null = any
    jurisdiction: string | null;   // ISO 3166-2, or null = any
    businessFlags: string[];
    weight: number;
    isRequired: boolean;
    reasonText: string;
  }>;
}

export const POLICY_CATALOG: CatalogEntry[] = [ ... ];

12 templates with sections and pre-wired rules:

# Template Category Default rules
1 Code of Conduct HR Any industry, any state · w:30 · not required
2 Payroll Policy HR Any industry, any state · w:20
3 Equal Employment Opportunity HR NAICS 62 · required · w:80; NAICS 62 · required · w:70; any · w:20
4 BYOD Policy IT Security remote_workers · w:40; byod_devices · w:60
5 Remote Work Security Policy IT Security remote_workers · w:30; NAICS 54 + US-CA + processes_pii · required · w:70
6 AI Acceptable Use Policy IT Security Any · w:25
7 IT Security Policy IT Security Any · w:40
8 HR Policy HR Any · w:20
9 Privacy and Data Protection Policy Data Privacy processes_pii · w:50; processes_pii + US-CA · required · w:80
10 Physical Security Policy Physical Security NAICS 32 · required · w:60; NAICS 33 · required · w:60
11 Health and Safety Policy Health & Safety NAICS 32 · required · w:90; NAICS 33 · required · w:90; NAICS 62 · w:50
12 Drug and Alcohol Policy HR NAICS 32 · w:70; NAICS 33 · w:70; NAICS 62 · w:40

NAICS coverage across the five target sectors:

NAICS Sector Required templates Recommended
621 Ambulatory Health Care EEO, Privacy Code of Conduct, IT Security, HR, AI Use
624 Social Assistance EEO, Health & Safety Code of Conduct, HR, Privacy, AI Use, Drug & Alcohol
3253 Agricultural Chemicals Health & Safety, Physical Security Drug & Alcohol, IT Security
3261 Plastics Manufacturing Health & Safety, Physical Security Drug & Alcohol, Code of Conduct
3312 Steel Manufacturing Health & Safety, Physical Security Drug & Alcohol, Code of Conduct

Sections, placeholder lists, and full rule reasonText values are written in db/policy-templates-catalog.ts. PLATFORM_ADMIN.md links to this file as the canonical content reference.

1.2 Seed script

New file: scripts/seed-policy-templates.ts

Behaviour: 1. Reads POLICY_CATALOG from the catalog file 2. For each entry: checks policy_templates collection for an existing document with the same titleskips if found (idempotent) 3. Creates the template document (status: draft, version: 0) 4. Immediately publishes it (status: published, version: 1, changelog entry: "Initial release from AssetGuard catalog") 5. Creates each rule document in template_rules 6. Calls invalidateRuleCache() once at the end 7. Prints a summary: ✓ Created 10 templates (2 already existed). Created 28 rules.

New script entry in package.json:

"seed:templates": "tsx read-env.ts && tsx scripts/seed-policy-templates.ts"

Usage after a fresh environment setup:

pnpm seed:templates

1.3 Platform-admin import wizard

New route: src/app/platform-admin/wizard/page.tsx
Hub card: Add "Import Catalog" card to src/app/platform-admin/page.tsx (claim: platform_admin)

The wizard is the browser-based counterpart to the seed script — it lets a platform admin review and import catalog templates without CLI access. Three steps:

Step 1 — Browse Catalog - Fetches catalog entries from a new internal endpoint: GET /api/platform-admin/catalog - This route reads POLICY_CATALOG at runtime and cross-references with Firestore to mark which entries already exist - Returns each entry with alreadyImported: boolean - Grid of cards, grouped by category - Each card shows: title, category, rule count ("3 rules pre-wired"), NAICS tags - Cards for already-imported templates are greyed out with a "Imported" badge - Multi-select checkboxes; "Select All New" shortcut button

Step 2 — Review - For each selected template: expandable section list (read-only preview) and rule list - Admin can edit title or category, toggle optional sections on/off, or adjust rule weights before import - "Back" returns to Step 1 without losing selections

Step 3 — Import - "Import N templates" button - Progress indicator: calls POST /api/platform-admin/catalog/import with the modified entries - Server creates templates (draft), publishes them, creates rules, invalidates cache - Results: "12 templates imported · 28 rules created" - "View Templates →" link to /platform-admin/policy-templates


Part 2 — AI-Assisted Template Creation

2.1 Remove Genkit

genkit and genkit-cli are in package.json but no source file imports them. Removal is clean.

pnpm remove genkit genkit-cli

Remove from package.json scripts:

"genkit:dev":   "genkit start -- tsx src/ai/dev.ts",
"genkit:watch": "genkit start -- tsx --watch src/ai/dev.ts",

2.2 Add Anthropic SDK

pnpm add @anthropic-ai/sdk

Add to .env and .env.example:

ANTHROPIC_API_KEY=sk-ant-...

New file: src/lib/ai/client.ts
Exports a singleton Anthropic client. Throws at import time if ANTHROPIC_API_KEY is missing (catches misconfiguration early).

2.3 Template generation API

New file: src/app/api/platform-admin/ai/generate-template/route.ts
Protected by withPlatformAdmin.

Request:

{ "description": "A policy for employees using personal devices to access company data", "category": "IT Security" }

Response: Server-Sent Events stream. Each event is a partial JSON patch. The client assembles the final object:

{ "title": "...", "sections": [...], "suggestedRules": [...] }

Model: claude-sonnet-4-6
Strategy: Tool use (tool_use content block) with a single generate_template tool that enforces the output schema. This avoids brittle JSON-in-prose parsing.

System prompt outline:

You are a compliance policy writer for AssetGuard, a multi-tenant risk management platform.

Output format:
- title: concise policy name
- sections[]: each has title, body (markdown, second-person, use {{PLACEHOLDER}} for company-specific values), isOptional
  Supported placeholders: {{COMPANY_NAME}}, {{EFFECTIVE_DATE}}, {{JURISDICTION}}, {{CONTACT_EMAIL}}, {{DEPARTMENT}}
- suggestedRules[]: each has industryCode (2-digit NAICS or null), jurisdiction (ISO 3166-2 or null),
  businessFlags (from the allowed list), weight (1–100), isRequired, reasonText

Allowed business flags: remote_workers, processes_pii, fleet_vehicles, byod_devices, drug_testing,
  handles_cash, operates_heavy_equipment, multi_site, public_facing

Write for a US-based audience. Be specific and actionable. Avoid legal disclaimers.

Rule inference in the prompt: The prompt explicitly instructs Claude to infer rules from the description. Examples given in the system prompt: - "HIPAA", "healthcare", "patient data" → NAICS 62, processes_pii, isRequired: true - "remote work", "home office" → remote_workers, weight 30 - "fleet", "driver", "vehicle" → fleet_vehicles, weight 50 - "personal device", "BYOD" → byod_devices, weight 60 - "California", "CCPA" → jurisdiction US-CA, processes_pii - "chemical", "manufacturing", "factory" → NAICS 32, safety-related flags

2.4 AI generation UI

Modified file: src/app/platform-admin/policy-templates/new/page.tsx

Add a tab bar at the top: "From Scratch" (current blank form) | "Generate with AI" (new)

"Generate with AI" tab — three-panel layout:

Left panel — Input (narrow): - Textarea: "Describe the policy…" (placeholder: e.g. "A policy for remote employees accessing company systems from personal devices") - Category dropdown (pre-filled from description if AI detects it) - "Generate" button

Centre panel — Sections preview: - Sections appear as they stream in — each section title appears first, then body fills in character by character - Each section has an inline edit button (opens the TipTap editor from TemplateEditorForm) - Optional sections have a toggle to include/exclude

Right panel — Suggested Rules: - Each suggested rule rendered as a card: industry badge, state badge, flag badges, weight slider, Required toggle, reason text - Accept / Reject buttons per rule - "Add custom rule" opens the RuleBuilderForm dialog

Footer: - "Save as Draft" → calls existing POST /api/platform-admin/policy-templates then creates accepted rules - "Start Over" → clears all state


File Inventory

New files

File Purpose
db/policy-templates-catalog.ts Canonical template + rule definitions (12 templates)
scripts/seed-policy-templates.ts CLI seed script — idempotent bulk import
src/app/api/policies/templates/[id]/route.ts Tenant-accessible published template read (GET only)
src/app/api/platform-admin/catalog/route.ts GET catalog with already-imported flags
src/app/api/platform-admin/catalog/import/route.ts POST bulk import from wizard Step 3
src/app/api/platform-admin/ai/generate-template/route.ts AI generation endpoint (SSE streaming)
src/app/platform-admin/wizard/page.tsx Import wizard (3-step)
src/lib/ai/client.ts Anthropic client singleton

Modified files

File Change
src/app/policies/new/page.tsx Add Step 0: TemplatePicker; pass template adoption data to PolicyForm
src/app/api/policies/route.ts Add sourceTemplateId, sourceVersion, sections[] to createSchema
src/lib/services/policy-service.ts Accept and persist template adoption fields in createPolicy
src/components/policies/TemplatePicker.tsx Add ?fallback=true mode; distinguish matched vs fallback items
src/app/api/policies/recommend/route.ts Support ?fallback=true query param
src/app/platform-admin/policy-templates/new/page.tsx Add "Generate with AI" tab
src/app/platform-admin/page.tsx Add "Import Catalog" hub card
package.json Remove genkit/genkit-cli; add @anthropic-ai/sdk; add seed:templates script
.env.example Add ANTHROPIC_API_KEY
docs/developer-guide/PLATFORM_ADMIN.md Link to db/policy-templates-catalog.ts as canonical content source; add Phase 1 and Phase 2 operating procedures

Implementation Sequence

The following order avoids building on top of broken foundations:

0.1  Wire TemplatePicker into /policies/new
0.2  Add sourceTemplateId + sections to createPolicy (API + service)
0.3  Add GET /api/policies/templates/:id (tenant-accessible template read)
0.4  TemplatePicker fallback when recommendations are empty
     ↓ at this point a tenant can pick a template and create a policy from it
1.1  Write db/policy-templates-catalog.ts  (12 templates + rules, full section content)
1.2  Write scripts/seed-policy-templates.ts
1.3  Build platform-admin import wizard
     ↓ the catalog is seeded; tenants see relevant recommendations
2.1  Remove Genkit; add @anthropic-ai/sdk
2.2  Write src/lib/ai/client.ts
2.3  Write generate-template API route (tool use, SSE)
2.4  Add "Generate with AI" tab to /platform-admin/policy-templates/new

Future Enhancements

Content-as-files: Markdown directory structure for template maintenance

The current approach embeds section HTML directly in db/policy-templates-catalog.ts as TypeScript template literals. This is acceptable for the initial catalog but becomes a maintenance burden as template text evolves — HTML in TS strings produces noisy diffs, requires a developer to edit, and is opaque to compliance reviewers.

Recommended future structure: one Markdown file per section, organized in named directories.

db/
  policy-templates/
    remote-work-security-policy/
      meta.json                   ← title, category, placeholders, rules[]
      01-purpose.md
      02-scope.md
      03-network-security.md
      04-data-handling.md
      05-incident-reporting.md
      06-compliance.md            ← isOptional: true (frontmatter)
    code-of-conduct/
      meta.json
      01-purpose.md
      ...

Each .md file carries its metadata in YAML frontmatter:

---
title: Network Security
isOptional: false
order: 3
---

Employees must use the company-provided VPN when accessing internal systems.
Use of public or unsecured Wi-Fi without VPN is prohibited.

Home routers must use WPA2 or WPA3 encryption. Default router credentials
must be changed. Contact {{CONTACT_EMAIL}} with questions.

The seed script would replace the static array with a directory scanner that uses gray-matter for frontmatter parsing and marked (or remark) to convert Markdown to TipTap-compatible HTML at seed time. db/policy-templates-catalog.ts either becomes a thin index of directory paths or is eliminated entirely.

What this enables: - A compliance officer or lawyer can open any .md file in VS Code, Obsidian, or the GitHub web editor and edit it with a Markdown preview. {{PLACEHOLDER}} tokens are fully visible. - PR diffs show exactly what changed in the policy text — not a wall of escaped HTML strings. - Adding a new template is "create a directory with a meta.json and some .md files" — no TypeScript changes needed. - The db/policy-templates-catalog.ts TypeScript catalog can be retired entirely or repurposed as a validation schema.

Trade-off: Requires adding gray-matter and marked (or remark) as dev dependencies, and the seed script becomes a directory scanner. If the HTML produced by marked diverges from TipTap's conventions (e.g. heading levels, list nesting), a post-processing step may be needed. Starting with .html files instead of .md is a valid alternative if the editing audience is technical.

Migration path from current approach: The existing POLICY_CATALOG entries can be exported as Markdown files by a one-time conversion script. The section HTML can be reverse-converted to Markdown using turndown. After the conversion, seed:templates would be updated to read from db/policy-templates/ directories and the TypeScript catalog file removed.


Open Questions

Before starting Phase 2 implementation:

  1. Streaming in Next.js App Router: The generate-template route uses Server-Sent Events. Verify that the platform-admin layout's withPlatformAdmin HOF is compatible with streaming responses (it should be, as it verifies the token synchronously before yielding to the handler, but worth confirming).

  2. Section body format: Template sections use TipTap rich-text (HTML). The AI will produce Markdown. A markdownToHtml conversion step is needed at the boundary before storing.

  3. Placeholder injection UX: When a tenant adopts a template and the sections contain {{COMPANY_NAME}} etc., should the form auto-replace known tokens before showing the editor, or show them as editable highlighted fields? The latter gives more control and is recommended.

  4. NAICS prefix matching in rules: The current matchTemplates function checks tenant.naicsCode.startsWith(rule.industryCode). The catalog uses 2-digit codes (e.g. 62). Confirm that tenants' naicsCode field is stored in a format that satisfies this check (e.g. 6211 starts with 62 ✓).