Documentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
Module: Platform (PF)
Feature ID: PF-64
Version: 1.0.0
Last Updated: 2026-01-30
Status: Production Ready
Executive Summary
The Organization Document Templates module provides centralized management of letterheads, document templates, and approval chain templates for cross-core document generation. This enables consistent, professional document output across GR (Governance & Risk), HR, and other modules.
Architecture Overview
Module Purpose and Scope
PF-64 provides:
- Letterhead Management: Organization branding for document headers/footers
- Document Templates: Structure templates for policies, procedures, letters
- Approval Chain Templates: Reusable approval workflows
- PDF Generation: Server-side document rendering with letterhead integration
Key Design Decisions
- Multi-tenant with System Templates: Organizations can create custom templates while also accessing system-provided templates
- Version Control: Document templates maintain immutable version history
- Status-based Deprecation: Templates use status fields instead of soft-delete to preserve audit trails
- Dynamic Approver Resolution: Approval chains integrate with HR module for manager/department head lookup
Database Design
Entity Relationship Diagram
┌─────────────────────┐ ┌─────────────────────────┐
│ pf_letterheads │ │ pf_document_templates │
├─────────────────────┤ ├─────────────────────────┤
│ id (PK) │ │ id (PK) │
│ organization_id (FK)│◄────┤ letterhead_id (FK) │
│ name │ │ organization_id (FK) │
│ logo_url │ │ name │
│ logo_position │ │ template_type │
│ organization_name │ │ status │
│ address_* │ │ sections (JSONB) │
│ primary_color │ │ config (JSONB) │
│ footer_text │ │ version │
│ is_default │ │ usage_count │
│ is_system │ │ is_default │
└─────────────────────┘ │ is_system │
└───────────┬─────────────┘
│
▼
┌───────────────────────────────────┐
│ pf_document_template_versions │
├───────────────────────────────────┤
│ id (PK) │
│ template_id (FK) │
│ version_number │
│ sections (JSONB) │
│ config (JSONB) │
│ change_notes │
│ created_by │
│ created_at │
└───────────────────────────────────┘
┌─────────────────────────────┐
│ pf_approval_chain_templates │
├─────────────────────────────┤
│ id (PK) │
│ organization_id (FK) │
│ name │
│ category │
│ steps (JSONB) │
│ usage_count │
│ is_default │
│ is_system │
└─────────────────────────────┘
Table Specifications
pf_letterheads
| Column | Type | Constraints | Description |
|---|
| id | UUID | PK, DEFAULT gen_random_uuid() | Unique identifier |
| organization_id | UUID | FK → pf_organizations, NULL for system | Owner organization |
| name | TEXT | NOT NULL | Display name |
| logo_url | TEXT | - | Logo image URL |
| logo_position | TEXT | ’left’, ‘center’, ‘right’ | Logo alignment |
| organization_name | TEXT | - | Org name for header |
| address_line_1 | TEXT | - | Street address |
| city | TEXT | - | City |
| state | TEXT | - | State/Province |
| zip_code | TEXT | - | Postal code |
| phone | TEXT | - | Contact phone |
| email | TEXT | - | Contact email |
| primary_color | TEXT | - | Brand color (hex) |
| secondary_color | TEXT | - | Accent color (hex) |
| footer_text | TEXT | - | Footer/confidentiality text |
| is_default | BOOLEAN | DEFAULT false | Default for org |
| is_system | BOOLEAN | DEFAULT false | System-provided |
pf_document_templates
| Column | Type | Constraints | Description |
|---|
| id | UUID | PK | Unique identifier |
| organization_id | UUID | FK, NULL for system | Owner organization |
| letterhead_id | UUID | FK → pf_letterheads | Associated letterhead |
| name | TEXT | NOT NULL | Template name |
| description | TEXT | - | Template description |
| template_type | TEXT | NOT NULL | policy, procedure, letter, report_cover, other |
| status | TEXT | DEFAULT ‘draft’ | draft, active, deprecated |
| sections | JSONB | NOT NULL | Section definitions |
| config | JSONB | DEFAULT '' | Template configuration |
| version | INTEGER | DEFAULT 1 | Current version number |
| usage_count | INTEGER | DEFAULT 0 | Times template used |
| is_default | BOOLEAN | DEFAULT false | Default for type |
| is_system | BOOLEAN | DEFAULT false | System-provided |
pf_document_template_versions
| Column | Type | Constraints | Description |
|---|
| id | UUID | PK | Version record ID |
| template_id | UUID | FK → pf_document_templates | Parent template |
| version_number | INTEGER | NOT NULL | Sequential version |
| sections | JSONB | NOT NULL | Sections at this version |
| config | JSONB | NOT NULL | Config at this version |
| change_notes | TEXT | - | Version change description |
| created_by | UUID | FK → pf_profiles | Version creator |
| created_at | TIMESTAMPTZ | NOT NULL | Version timestamp |
pf_approval_chain_templates
| Column | Type | Constraints | Description |
|---|
| id | UUID | PK | Unique identifier |
| organization_id | UUID | FK, NULL for system | Owner organization |
| name | TEXT | NOT NULL | Chain name |
| description | TEXT | - | Chain description |
| category | TEXT | NOT NULL | policy, document, financial, hr, compliance, general |
| steps | JSONB | NOT NULL | Approval step definitions |
| usage_count | INTEGER | DEFAULT 0 | Times chain applied |
| is_default | BOOLEAN | DEFAULT false | Default for category |
| is_system | BOOLEAN | DEFAULT false | System-provided |
JSONB Schemas
Section Definition
interface TemplateSection {
name: string;
help_text?: string;
required?: boolean;
placeholder?: string;
}
Template Config
interface TemplateConfig {
numbering_style?: 'decimal' | 'alpha' | 'roman' | 'none';
show_approval_block?: boolean;
default_watermark?: string;
}
Approval Step
interface ApprovalStep {
step_order: number;
name: string;
approver_type: 'user' | 'role' | 'dynamic' | 'field';
approver_config: {
userId?: string;
role?: string;
resolver?: 'manager' | 'department_head';
fieldPath?: string;
};
timeout_hours?: number;
reminder_hours?: number;
allow_delegate?: boolean;
}
Index Strategy
-- Letterheads
CREATE INDEX idx_pf_letterheads_org ON pf_letterheads(organization_id);
CREATE INDEX idx_pf_letterheads_default ON pf_letterheads(organization_id, is_default) WHERE is_default = true;
-- Document Templates
CREATE INDEX idx_pf_document_templates_org ON pf_document_templates(organization_id);
CREATE INDEX idx_pf_document_templates_type ON pf_document_templates(organization_id, template_type);
CREATE INDEX idx_pf_document_templates_status ON pf_document_templates(organization_id, status);
CREATE INDEX idx_pf_document_templates_default ON pf_document_templates(organization_id, template_type, is_default) WHERE is_default = true;
-- Template Versions
CREATE INDEX idx_pf_template_versions_template ON pf_document_template_versions(template_id);
CREATE INDEX idx_pf_template_versions_number ON pf_document_template_versions(template_id, version_number DESC);
-- Approval Chain Templates
CREATE INDEX idx_pf_approval_chains_org ON pf_approval_chain_templates(organization_id);
CREATE INDEX idx_pf_approval_chains_category ON pf_approval_chain_templates(organization_id, category);
CREATE INDEX idx_pf_approval_chains_default ON pf_approval_chain_templates(organization_id, category, is_default) WHERE is_default = true;
RLS Policy Summary
Multi-Tenant Isolation
All tables implement organization-based RLS:
-- Pattern for org-scoped SELECT
CREATE POLICY "select_org_or_system" ON table_name
FOR SELECT USING (
organization_id IS NULL -- System templates visible to all
OR organization_id IN (
SELECT organization_id FROM pf_user_organizations
WHERE profile_id = auth.uid()
)
);
System Template Protection
System templates (is_system = true) are read-only:
-- Prevent modification of system templates
CREATE POLICY "update_non_system" ON table_name
FOR UPDATE USING (
is_system = false
AND organization_id IN (SELECT ...)
) WITH CHECK (
is_system = false
AND organization_id = OLD.organization_id -- Prevent org_id change
);
Version History Immutability
Template versions are append-only:
-- No UPDATE or DELETE policies on pf_document_template_versions
CREATE POLICY "versions_insert" ON pf_document_template_versions
FOR INSERT WITH CHECK (
EXISTS (
SELECT 1 FROM pf_document_templates t
WHERE t.id = template_id
AND t.organization_id IN (SELECT ...)
)
);
CREATE POLICY "versions_select" ON pf_document_template_versions
FOR SELECT USING (
EXISTS (
SELECT 1 FROM pf_document_templates t
WHERE t.id = template_id
AND (t.organization_id IS NULL OR t.organization_id IN (SELECT ...))
)
);
Hook Architecture
Query Key Factory Pattern
// src/platform/templates/hooks/queryKeys.ts
export const templateKeys = {
all: ['templates'] as const,
letterheads: {
all: () => [...templateKeys.all, 'letterheads'] as const,
list: (orgId: string, filters?: LetterheadFilters) =>
[...templateKeys.letterheads.all(), orgId, filters] as const,
detail: (id: string) =>
[...templateKeys.letterheads.all(), 'detail', id] as const,
default: (orgId: string) =>
[...templateKeys.letterheads.all(), 'default', orgId] as const,
},
documents: {
all: () => [...templateKeys.all, 'documents'] as const,
list: (orgId: string, filters?: DocumentTemplateFilters) =>
[...templateKeys.documents.all(), orgId, filters] as const,
detail: (id: string) =>
[...templateKeys.documents.all(), 'detail', id] as const,
versions: (templateId: string) =>
[...templateKeys.documents.all(), 'versions', templateId] as const,
},
approvalChains: {
all: () => [...templateKeys.all, 'approvalChains'] as const,
list: (orgId: string, filters?: ApprovalChainFilters) =>
[...templateKeys.approvalChains.all(), orgId, filters] as const,
detail: (id: string) =>
[...templateKeys.approvalChains.all(), 'detail', id] as const,
default: (orgId: string, category: string) =>
[...templateKeys.approvalChains.all(), 'default', orgId, category] as const,
},
};
Mutation Pattern with Cache Invalidation
export function useLetterheadMutation() {
const queryClient = useQueryClient();
const { currentOrganization } = useOrganization();
const create = useMutation({
mutationFn: async (data: CreateLetterheadInput) => {
const { data: result, error } = await supabase
.from('pf_letterheads')
.insert({ ...data, organization_id: currentOrganization?.id })
.select()
.single();
if (error) throw error;
return result;
},
onSuccess: () => {
// Invalidate all letterhead queries for this org
queryClient.invalidateQueries({
queryKey: templateKeys.letterheads.all(),
});
},
});
return { create, update, remove, setDefault };
}
Error Handling Conventions
// Consistent error handling across all hooks
const { data, error, isLoading } = useQuery({
queryKey: templateKeys.letterheads.detail(id),
queryFn: async () => {
const { data, error } = await supabase
.from('pf_letterheads')
.select('*')
.eq('id', id)
.maybeSingle(); // Use maybeSingle to handle not found
if (error) throw error;
return data;
},
enabled: !!id, // Disable when ID missing
retry: (failureCount, error) => {
// Don't retry on 404/not found
if (error.code === 'PGRST116') return false;
return failureCount < 3;
},
});
Edge Function Design
generate-templated-pdf Architecture
┌─────────────────────────────────────────────────────────────┐
│ Edge Function Request │
├─────────────────────────────────────────────────────────────┤
│ { │
│ organizationId: string, │
│ templateId?: string, │
│ letterheadId?: string, │
│ content: { title, subtitle?, sections, metadata? }, │
│ options?: { showApprovalBlock, watermark, orientation } │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Request Validation │
│ - Validate required fields (organizationId, content.title) │
│ - Validate options schema │
│ - Verify user org membership │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Fetch Dependencies │
│ - Fetch letterhead (if letterheadId or use default) │
│ - Fetch template config (if templateId) │
│ - Fetch logo image (if letterhead.logo_url) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PDF Generation (pdf-lib) │
│ 1. Create PDFDocument │
│ 2. Add page(s) with orientation │
│ 3. Render letterhead header (logo, org info) │
│ 4. Render document title/subtitle │
│ 5. Render numbered sections with content │
│ 6. Render approval block (if enabled) │
│ 7. Render letterhead footer on all pages │
│ 8. Add watermark (if specified) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Storage & Record Creation │
│ 1. Upload PDF to pf-documents bucket │
│ 2. Create pf_documents record │
│ 3. Generate signed URL (1 hour expiry) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Response │
│ { │
│ success: true, │
│ documentId: string, │
│ url: string (signed), │
│ storagePath: string, │
│ fileName: string, │
│ fileSize: number, │
│ pageCount: number, │
│ generationTimeMs: number │
│ } │
└─────────────────────────────────────────────────────────────┘
PDF-lib Usage Patterns
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
// Document creation
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([612, 792]); // Letter size
// Font embedding
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
// Logo embedding (PNG/JPEG only)
const logoBytes = await fetch(logoUrl).then(r => r.arrayBuffer());
const logo = logoUrl.endsWith('.png')
? await pdfDoc.embedPng(logoBytes)
: await pdfDoc.embedJpg(logoBytes);
// Text rendering with wrapping
const drawWrappedText = (text: string, x: number, y: number, maxWidth: number) => {
const lines = wrapText(text, maxWidth, fontSize);
let currentY = y;
for (const line of lines) {
page.drawText(line, { x, y: currentY, font, size: fontSize });
currentY -= lineHeight;
}
return currentY;
};
| Aspect | Target | Implementation |
|---|
| Generation Time | < 3 seconds | Parallel fetches, efficient rendering |
| Memory | < 128MB | Stream large images, cleanup |
| Concurrent Requests | 10+ | Stateless function, no shared state |
| Large Documents | 50+ pages | Pagination logic, memory management |
Security Model
Permission Requirements
| Permission | Description | Default Roles |
|---|
| pf.templates.view | View templates | org_admin, manager, staff |
| pf.templates.manage | Create/edit/delete templates | org_admin |
Multi-Tenant Isolation
- RLS on all tables: Every query filtered by organization_id
- System template protection: is_system templates read-only
- Edge function validation: Verify org membership before processing
- Storage bucket policies: Organization-scoped paths
Data Protection
- No PHI in templates: Templates contain structure, not patient data
- Audit trail: Version history preserved, no hard deletes
- Signed URLs: Time-limited access to generated documents
Consumer Integration Guide
GR-01 Policy Integration
import {
useDocumentTemplate,
useGenerateTemplatedPdf,
PolicyExportDialog
} from '@/platform/templates';
function PolicyDetailPage({ policy }) {
const { data: template } = useDocumentTemplate(undefined, undefined, 'policy');
const { generatePdf, isGenerating } = useGenerateTemplatedPdf();
const handleExport = async (letterheadId: string, templateId?: string) => {
const result = await generatePdf({
letterheadId,
templateId,
content: {
title: policy.title,
sections: [
{ name: 'Purpose', content: policy.purpose },
{ name: 'Scope', content: policy.scope },
{ name: 'Policy Statement', content: policy.statement },
],
metadata: {
documentNumber: policy.policy_number,
version: policy.version,
effectiveDate: policy.effective_date,
},
},
options: {
showApprovalBlock: true,
},
});
if (result.url) {
window.open(result.url, '_blank');
}
};
return (
<PolicyExportDialog
policy={policy}
onExport={handleExport}
isExporting={isGenerating}
/>
);
}
GR-11 Procedure Integration
import { useDocumentTemplate, useGenerateTemplatedPdf } from '@/platform/templates';
function ProcedureExport({ procedure }) {
const { generatePdf } = useGenerateTemplatedPdf();
const handleExport = async () => {
await generatePdf({
content: {
title: procedure.title,
subtitle: 'Standard Operating Procedure',
sections: procedure.steps.map((step, idx) => ({
name: `Step ${idx + 1}: ${step.name}`,
content: step.description,
})),
},
});
};
}
HR Offer Letter Integration
import { useLetterhead, useGenerateTemplatedPdf } from '@/platform/templates';
function OfferLetterGenerator({ candidate, offer }) {
const { data: letterhead } = useLetterhead();
const { generatePdf } = useGenerateTemplatedPdf();
const handleGenerate = async () => {
await generatePdf({
letterheadId: letterhead?.id,
content: {
title: 'Offer of Employment',
sections: [
{ name: 'Position', content: `We are pleased to offer you the position of ${offer.title}.` },
{ name: 'Compensation', content: `Annual salary: $${offer.salary.toLocaleString()}` },
{ name: 'Start Date', content: `Your anticipated start date is ${offer.startDate}.` },
],
metadata: {
author: 'Human Resources',
effectiveDate: offer.startDate,
},
},
options: {
showApprovalBlock: true,
},
});
};
}
Custom Integration Pattern
// Create a custom export hook for your module
function useMyModuleExport() {
const { generatePdf, isGenerating } = useGenerateTemplatedPdf();
const { data: letterhead } = useLetterhead();
const { data: template } = useDocumentTemplate(undefined, undefined, 'other');
const exportDocument = async (entity: MyEntity) => {
const sections = transformEntityToSections(entity);
const result = await generatePdf({
letterheadId: letterhead?.id,
templateId: template?.id,
content: {
title: entity.name,
sections,
metadata: {
documentNumber: entity.reference_number,
},
},
});
return result;
};
return { exportDocument, isExporting: isGenerating };
}
Testing Strategy
Test Files
| File | Type | Coverage |
|---|
| tests/rls/pf-letterheads.rls.test.ts | RLS | Multi-tenant isolation |
| tests/rls/pf-document-templates.rls.test.ts | RLS | CRUD + versioning |
| tests/rls/pf-approval-chain-templates.rls.test.ts | RLS | Chain lifecycle |
| tests/unit/platform/templates/useLetterhead.test.ts | Unit | Query/mutation |
| tests/unit/platform/templates/useDocumentTemplate.test.ts | Unit | Versioning |
| tests/unit/platform/templates/useApprovalChainTemplate.test.ts | Unit | Apply workflow |
| tests/unit/platform/templates/components.test.tsx | Unit | UI components |
| tests/integration/platform/templates-workflow.test.ts | Integration | E2E lifecycle |
| tests/integration/platform/pdf-generation.test.ts | Integration | PDF service |
| supabase/functions/generate-templated-pdf/index.test.ts | Edge | PDF logic |
Coverage Targets
- RLS Tests: 100%
- Unit Tests: 80%+
- Integration Tests: Critical paths covered
Appendix
Feature Flag
Enable via organization settings:
UPDATE pf_organizations
SET settings = jsonb_set(settings, '{pf_templates_enabled}', 'true')
WHERE id = 'org-id';
- GR-01: Policy Management (consumer)
- GR-11: Procedure Management (consumer)
- HR: Offer Letters (consumer)
- FW: Workflow Framework (approval integration)
Migration Notes
PF-64 was implemented in phases:
- Phase 1: Database schema and types
- Phase 2: RLS policies
- Phase 3: React hooks and query layer
- Phase 4: PDF generation edge function
- Phase 5: Testing and documentation
Document Version History
| Version | Date | Author | Changes |
|---|
| 1.0.0 | 2026-01-30 | AI | Initial architecture document |