Skip to content

Templates

Pocketbook's template system enables organizations to standardize workflows, automate document generation, and customize user experiences at scale.

Overview

Templates provide:

  • Workflow automation: Standardize repeating processes
  • Document generation: Create consistent, branded documents
  • Email customization: Personalized communication templates
  • Report templates: Automated reporting with custom formatting
  • Invoice templates: Professional, branded invoices
  • Integration templates: Pre-configured third-party integrations

Template Types

1. Document Templates

Create templates for invoices, receipts, and reports:

typescript
// src/templates/document.ts
interface DocumentTemplate {
  id: string;
  name: string;
  type: 'invoice' | 'receipt' | 'report' | 'contract';
  format: 'pdf' | 'html' | 'docx';
  template: string;
  variables: TemplateVariable[];
  styling: TemplateStyle;
}

interface TemplateVariable {
  name: string;
  type: 'string' | 'number' | 'date' | 'currency';
  required: boolean;
  defaultValue?: any;
}

// Example invoice template
const invoiceTemplate: DocumentTemplate = {
  id: 'tpl_invoice_001',
  name: 'Standard Invoice',
  type: 'invoice',
  format: 'pdf',
  template: `
    <!DOCTYPE html>
    <html>
      <head>
        <style>
          .invoice { font-family: Arial, sans-serif; }
          .header { background: {{brandColor}}; color: white; padding: 20px; }
          .items { margin: 20px 0; }
          .total { font-weight: bold; font-size: 18px; }
        </style>
      </head>
      <body>
        <div class="invoice">
          <div class="header">
            <h1>{{companyName}}</h1>
            <p>Invoice #{{invoiceNumber}}</p>
          </div>

          <div class="customer">
            <h3>Bill To:</h3>
            <p>{{customerName}}</p>
            <p>{{customerAddress}}</p>
          </div>

          <div class="items">
            <table>
              <tr>
                <th>Description</th>
                <th>Quantity</th>
                <th>Price</th>
                <th>Total</th>
              </tr>
              {{#each items}}
              <tr>
                <td>{{description}}</td>
                <td>{{quantity}}</td>
                <td>{{price}}</td>
                <td>{{total}}</td>
              </tr>
              {{/each}}
            </table>
          </div>

          <div class="total">
            Total: {{currency}} {{totalAmount}}
          </div>
        </div>
      </body>
    </html>
  `,
  variables: [
    { name: 'companyName', type: 'string', required: true },
    { name: 'invoiceNumber', type: 'string', required: true },
    { name: 'customerName', type: 'string', required: true },
    { name: 'totalAmount', type: 'currency', required: true },
  ],
  styling: {
    brandColor: '#3B82F6',
    fontSize: '14px',
    fontFamily: 'Arial, sans-serif',
  },
};

2. Email Templates

Standardize email communications:

typescript
// src/templates/email.ts
interface EmailTemplate {
  id: string;
  name: string;
  subject: string;
  body: string;
  variables: TemplateVariable[];
  attachments?: AttachmentConfig[];
}

const welcomeEmailTemplate: EmailTemplate = {
  id: 'email_welcome_001',
  name: 'Welcome Email',
  subject: 'Welcome to {{companyName}}, {{userName}}!',
  body: `
    <html>
      <body style="font-family: Arial, sans-serif;">
        <div style="max-width: 600px; margin: 0 auto;">
          <h1 style="color: {{brandColor}};">Welcome, {{userName}}!</h1>

          <p>Thank you for joining {{companyName}}. We're excited to have you on board!</p>

          <div style="background: #f5f5f5; padding: 20px; margin: 20px 0;">
            <h3>Get Started:</h3>
            <ul>
              <li>Complete your profile</li>
              <li>Verify your email</li>
              <li>Explore our features</li>
            </ul>
          </div>

          <a href="{{dashboardUrl}}" style="background: {{brandColor}}; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
            Go to Dashboard
          </a>

          <p style="margin-top: 30px; color: #666;">
            Need help? Contact us at {{supportEmail}}
          </p>
        </div>
      </body>
    </html>
  `,
  variables: [
    { name: 'userName', type: 'string', required: true },
    { name: 'companyName', type: 'string', required: true },
    { name: 'dashboardUrl', type: 'string', required: true },
    { name: 'supportEmail', type: 'string', required: true },
  ],
};

3. Workflow Templates

Automate business processes:

typescript
// src/templates/workflow.ts
interface WorkflowTemplate {
  id: string;
  name: string;
  trigger: WorkflowTrigger;
  steps: WorkflowStep[];
  conditions?: WorkflowCondition[];
}

interface WorkflowStep {
  id: string;
  type: 'action' | 'notification' | 'approval' | 'delay';
  config: any;
  onSuccess?: string; // Next step ID
  onFailure?: string; // Fallback step ID
}

const paymentWorkflow: WorkflowTemplate = {
  id: 'wf_payment_001',
  name: 'Payment Processing Workflow',
  trigger: {
    event: 'transaction.created',
    conditions: [
      { field: 'amount', operator: 'gte', value: 1000 },
    ],
  },
  steps: [
    {
      id: 'step_1',
      type: 'action',
      config: {
        action: 'verify_payment_method',
      },
      onSuccess: 'step_2',
      onFailure: 'step_notify_failure',
    },
    {
      id: 'step_2',
      type: 'approval',
      config: {
        approvers: ['finance@company.com'],
        timeout: 24 * 60 * 60, // 24 hours
        message: 'Payment of {{amount}} requires approval',
      },
      onSuccess: 'step_3',
      onFailure: 'step_notify_rejection',
    },
    {
      id: 'step_3',
      type: 'action',
      config: {
        action: 'process_payment',
      },
      onSuccess: 'step_notify_success',
      onFailure: 'step_notify_failure',
    },
    {
      id: 'step_notify_success',
      type: 'notification',
      config: {
        template: 'payment_success',
        recipients: ['{{userEmail}}', 'finance@company.com'],
      },
    },
    {
      id: 'step_notify_failure',
      type: 'notification',
      config: {
        template: 'payment_failed',
        recipients: ['{{userEmail}}', 'support@company.com'],
      },
    },
  ],
};

Template Management API

Create Template

http
POST /api/v1/templates
Content-Type: application/json

{
  "name": "Monthly Report",
  "type": "document",
  "format": "pdf",
  "template": "<html>...</html>",
  "variables": [...]
}

List Templates

http
GET /api/v1/templates?type=invoice&format=pdf

Get Template

http
GET /api/v1/templates/{template_id}

Update Template

http
PATCH /api/v1/templates/{template_id}
Content-Type: application/json

{
  "name": "Updated Monthly Report",
  "template": "<html>...</html>"
}

Delete Template

http
DELETE /api/v1/templates/{template_id}

Rendering Templates

Render Document

typescript
// src/services/template-renderer.ts
import Handlebars from 'handlebars';
import puppeteer from 'puppeteer';

class TemplateRenderer {
  async renderDocument(
    templateId: string,
    data: Record<string, any>
  ): Promise<Buffer> {
    const template = await db.templates.findById(templateId);

    if (!template) {
      throw new Error('Template not found');
    }

    // Compile template
    const compiled = Handlebars.compile(template.template);
    const html = compiled(data);

    // Convert to PDF if needed
    if (template.format === 'pdf') {
      return this.htmlToPdf(html);
    }

    return Buffer.from(html);
  }

  private async htmlToPdf(html: string): Promise<Buffer> {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    await page.setContent(html);

    const pdf = await page.pdf({
      format: 'A4',
      printBackground: true,
      margin: {
        top: '20px',
        right: '20px',
        bottom: '20px',
        left: '20px',
      },
    });

    await browser.close();

    return pdf;
  }

  async renderEmail(
    templateId: string,
    data: Record<string, any>
  ): Promise<{ subject: string; body: string }> {
    const template = await db.emailTemplates.findById(templateId);

    const subjectCompiled = Handlebars.compile(template.subject);
    const bodyCompiled = Handlebars.compile(template.body);

    return {
      subject: subjectCompiled(data),
      body: bodyCompiled(data),
    };
  }
}

export const templateRenderer = new TemplateRenderer();

// Usage
const invoice = await templateRenderer.renderDocument('tpl_invoice_001', {
  companyName: 'Pocketbook Inc.',
  invoiceNumber: 'INV-2024-001',
  customerName: 'John Doe',
  customerAddress: '123 Main St, City, ST 12345',
  items: [
    { description: 'Premium Plan', quantity: 1, price: 99.99, total: 99.99 },
  ],
  currency: 'USD',
  totalAmount: 99.99,
  brandColor: '#3B82F6',
});

// Save or send invoice
await saveInvoice(invoice);

Send Templated Email

typescript
// src/services/email-sender.ts
import { templateRenderer } from './template-renderer';

export async function sendTemplatedEmail(
  templateId: string,
  to: string,
  data: Record<string, any>
) {
  const { subject, body } = await templateRenderer.renderEmail(templateId, data);

  await emailClient.send({
    to,
    subject,
    html: body,
  });
}

// Usage
await sendTemplatedEmail('email_welcome_001', 'user@example.com', {
  userName: 'John Doe',
  companyName: 'Pocketbook',
  dashboardUrl: 'https://app.pocketbook.studio/dashboard',
  supportEmail: 'support@pocketbook.studio',
  brandColor: '#3B82F6',
});

Custom Helper Functions

typescript
// src/templates/helpers.ts
import Handlebars from 'handlebars';

// Register custom helpers
Handlebars.registerHelper('formatCurrency', function (amount: number, currency: string) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
  }).format(amount / 100);
});

Handlebars.registerHelper('formatDate', function (date: Date, format: string) {
  return new Intl.DateTimeFormat('en-US', {
    dateStyle: format as any,
  }).format(new Date(date));
});

Handlebars.registerHelper('uppercase', function (str: string) {
  return str.toUpperCase();
});

Handlebars.registerHelper('if_eq', function (a: any, b: any, options: any) {
  if (a === b) {
    return options.fn(this);
  }
  return options.inverse(this);
});

// Usage in templates
// {{formatCurrency totalAmount "USD"}}
// {{formatDate createdAt "long"}}
// {{uppercase companyName}}
// {{#if_eq status "active"}}Active{{/if_eq}}

Template Versioning

typescript
// src/models/template-version.ts
interface TemplateVersion {
  id: string;
  templateId: string;
  version: number;
  template: string;
  variables: TemplateVariable[];
  createdAt: Date;
  createdBy: string;
  changelog?: string;
}

export async function createTemplateVersion(
  templateId: string,
  changes: Partial<DocumentTemplate>,
  userId: string
): Promise<TemplateVersion> {
  const currentTemplate = await db.templates.findById(templateId);
  const latestVersion = await db.templateVersions.findOne(
    { templateId },
    { sort: { version: -1 } }
  );

  const newVersion: TemplateVersion = {
    id: crypto.randomUUID(),
    templateId,
    version: (latestVersion?.version || 0) + 1,
    template: changes.template || currentTemplate.template,
    variables: changes.variables || currentTemplate.variables,
    createdAt: new Date(),
    createdBy: userId,
    changelog: changes.changelog,
  };

  await db.templateVersions.create(newVersion);
  await db.templates.update(templateId, changes);

  return newVersion;
}

// Rollback to previous version
export async function rollbackTemplate(
  templateId: string,
  version: number
): Promise<void> {
  const targetVersion = await db.templateVersions.findOne({
    templateId,
    version,
  });

  if (!targetVersion) {
    throw new Error('Version not found');
  }

  await db.templates.update(templateId, {
    template: targetVersion.template,
    variables: targetVersion.variables,
  });
}

Template Testing

typescript
// src/services/template-tester.ts
export async function testTemplate(
  templateId: string,
  testData: Record<string, any>
): Promise<{ success: boolean; output?: string; errors?: string[] }> {
  try {
    const output = await templateRenderer.renderDocument(templateId, testData);

    return {
      success: true,
      output: output.toString('base64'),
    };
  } catch (error) {
    return {
      success: false,
      errors: [error.message],
    };
  }
}

// Validate template syntax
export function validateTemplate(template: string): { valid: boolean; errors: string[] } {
  const errors: string[] = [];

  try {
    Handlebars.compile(template);
  } catch (error) {
    errors.push(`Syntax error: ${error.message}`);
  }

  // Check for required variables
  const variablePattern = /\{\{([^}]+)\}\}/g;
  const matches = template.match(variablePattern) || [];

  return {
    valid: errors.length === 0,
    errors,
  };
}

Template Marketplace

typescript
// Pre-built templates available for all users
export const templateMarketplace = [
  {
    id: 'marketplace_invoice_modern',
    name: 'Modern Invoice',
    category: 'invoice',
    preview: '/previews/modern-invoice.png',
    description: 'Clean, modern invoice template',
    price: 0, // Free
  },
  {
    id: 'marketplace_receipt_minimal',
    name: 'Minimal Receipt',
    category: 'receipt',
    preview: '/previews/minimal-receipt.png',
    description: 'Simple, minimal receipt template',
    price: 0,
  },
  {
    id: 'marketplace_report_executive',
    name: 'Executive Report',
    category: 'report',
    preview: '/previews/executive-report.png',
    description: 'Professional executive report template',
    price: 9.99,
  },
];

// Install template from marketplace
export async function installMarketplaceTemplate(
  marketplaceId: string,
  userId: string
): Promise<string> {
  const marketplaceTemplate = templateMarketplace.find(
    (t) => t.id === marketplaceId
  );

  if (!marketplaceTemplate) {
    throw new Error('Template not found in marketplace');
  }

  // Create copy for user
  const userTemplate = await db.templates.create({
    ...marketplaceTemplate,
    id: crypto.randomUUID(),
    userId,
    isMarketplace: false,
    createdAt: new Date(),
  });

  return userTemplate.id;
}

Access Control

typescript
// src/middleware/template-access.ts
export async function checkTemplateAccess(
  userId: string,
  templateId: string,
  action: 'read' | 'write' | 'delete'
): Promise<boolean> {
  const template = await db.templates.findById(templateId);

  if (!template) {
    return false;
  }

  // Check ownership
  if (template.userId === userId) {
    return true;
  }

  // Check team access
  const teamAccess = await db.templateAccess.findOne({
    templateId,
    userId,
  });

  if (teamAccess && teamAccess.permissions.includes(action)) {
    return true;
  }

  return false;
}

// Share template with team
export async function shareTemplate(
  templateId: string,
  userIds: string[],
  permissions: ('read' | 'write' | 'delete')[]
): Promise<void> {
  for (const userId of userIds) {
    await db.templateAccess.upsert(
      { templateId, userId },
      { permissions, sharedAt: new Date() }
    );
  }
}

Best Practices

  1. Version control: Always version templates for rollback capability
  2. Test thoroughly: Test templates with various data sets
  3. Use variables: Make templates reusable with variables
  4. Validate input: Ensure all required variables are provided
  5. Optimize performance: Cache compiled templates
  6. Security: Sanitize user input in templates
  7. Documentation: Document all template variables
  8. Branding consistency: Use consistent styling across templates
  9. Mobile-friendly: Ensure email templates are responsive
  10. A/B testing: Test different template versions

Performance Optimization

typescript
// Cache compiled templates
import LRU from 'lru-cache';

const templateCache = new LRU<string, HandlebarsTemplateDelegate>({
  max: 100,
  ttl: 1000 * 60 * 60, // 1 hour
});

export function getCompiledTemplate(template: string): HandlebarsTemplateDelegate {
  const cacheKey = crypto.createHash('sha256').update(template).digest('hex');

  let compiled = templateCache.get(cacheKey);

  if (!compiled) {
    compiled = Handlebars.compile(template);
    templateCache.set(cacheKey, compiled);
  }

  return compiled;
}

Next Steps

Resources

Released under the MIT License.