Skip to main content

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

  1. Multi-tenant with System Templates: Organizations can create custom templates while also accessing system-provided templates
  2. Version Control: Document templates maintain immutable version history
  3. Status-based Deprecation: Templates use status fields instead of soft-delete to preserve audit trails
  4. 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

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()Unique identifier
organization_idUUIDFK → pf_organizations, NULL for systemOwner organization
nameTEXTNOT NULLDisplay name
logo_urlTEXT-Logo image URL
logo_positionTEXT’left’, ‘center’, ‘right’Logo alignment
organization_nameTEXT-Org name for header
address_line_1TEXT-Street address
cityTEXT-City
stateTEXT-State/Province
zip_codeTEXT-Postal code
phoneTEXT-Contact phone
emailTEXT-Contact email
primary_colorTEXT-Brand color (hex)
secondary_colorTEXT-Accent color (hex)
footer_textTEXT-Footer/confidentiality text
is_defaultBOOLEANDEFAULT falseDefault for org
is_systemBOOLEANDEFAULT falseSystem-provided

pf_document_templates

ColumnTypeConstraintsDescription
idUUIDPKUnique identifier
organization_idUUIDFK, NULL for systemOwner organization
letterhead_idUUIDFK → pf_letterheadsAssociated letterhead
nameTEXTNOT NULLTemplate name
descriptionTEXT-Template description
template_typeTEXTNOT NULLpolicy, procedure, letter, report_cover, other
statusTEXTDEFAULT ‘draft’draft, active, deprecated
sectionsJSONBNOT NULLSection definitions
configJSONBDEFAULT ''Template configuration
versionINTEGERDEFAULT 1Current version number
usage_countINTEGERDEFAULT 0Times template used
is_defaultBOOLEANDEFAULT falseDefault for type
is_systemBOOLEANDEFAULT falseSystem-provided

pf_document_template_versions

ColumnTypeConstraintsDescription
idUUIDPKVersion record ID
template_idUUIDFK → pf_document_templatesParent template
version_numberINTEGERNOT NULLSequential version
sectionsJSONBNOT NULLSections at this version
configJSONBNOT NULLConfig at this version
change_notesTEXT-Version change description
created_byUUIDFK → pf_profilesVersion creator
created_atTIMESTAMPTZNOT NULLVersion timestamp

pf_approval_chain_templates

ColumnTypeConstraintsDescription
idUUIDPKUnique identifier
organization_idUUIDFK, NULL for systemOwner organization
nameTEXTNOT NULLChain name
descriptionTEXT-Chain description
categoryTEXTNOT NULLpolicy, document, financial, hr, compliance, general
stepsJSONBNOT NULLApproval step definitions
usage_countINTEGERDEFAULT 0Times chain applied
is_defaultBOOLEANDEFAULT falseDefault for category
is_systemBOOLEANDEFAULT falseSystem-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;
};

Performance Considerations

AspectTargetImplementation
Generation Time< 3 secondsParallel fetches, efficient rendering
Memory< 128MBStream large images, cleanup
Concurrent Requests10+Stateless function, no shared state
Large Documents50+ pagesPagination logic, memory management

Security Model

Permission Requirements

PermissionDescriptionDefault Roles
pf.templates.viewView templatesorg_admin, manager, staff
pf.templates.manageCreate/edit/delete templatesorg_admin

Multi-Tenant Isolation

  1. RLS on all tables: Every query filtered by organization_id
  2. System template protection: is_system templates read-only
  3. Edge function validation: Verify org membership before processing
  4. Storage bucket policies: Organization-scoped paths

Data Protection

  1. No PHI in templates: Templates contain structure, not patient data
  2. Audit trail: Version history preserved, no hard deletes
  3. 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

FileTypeCoverage
tests/rls/pf-letterheads.rls.test.tsRLSMulti-tenant isolation
tests/rls/pf-document-templates.rls.test.tsRLSCRUD + versioning
tests/rls/pf-approval-chain-templates.rls.test.tsRLSChain lifecycle
tests/unit/platform/templates/useLetterhead.test.tsUnitQuery/mutation
tests/unit/platform/templates/useDocumentTemplate.test.tsUnitVersioning
tests/unit/platform/templates/useApprovalChainTemplate.test.tsUnitApply workflow
tests/unit/platform/templates/components.test.tsxUnitUI components
tests/integration/platform/templates-workflow.test.tsIntegrationE2E lifecycle
tests/integration/platform/pdf-generation.test.tsIntegrationPDF service
supabase/functions/generate-templated-pdf/index.test.tsEdgePDF 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
VersionDateAuthorChanges
1.0.02026-01-30AIInitial architecture document