Skip to content

Document Generation

The @openinsure/documents package generates all insurance documents as HTML. Production PDFs are rendered by the WeasyPrint doc factory, then stored with audit metadata for retrieval, e-signature, and archival.


Policy Documents

Declarations page, policy jacket, endorsement certificate, binder

Sales & Quotes

Quote proposal, premium indication letter, coverage comparison

Certificates

Certificate of Insurance (COI / ACORD 25), MCS-90 motor carrier certificate

Billing

Invoice, installment schedule, check (disbursement)

Claims

Reservation of rights letter, coverage denial, settlement agreement, proof of loss

Compliance

Notice of Cancellation (NOC), non-renewal notice, loss run, bordereaux report


Every document type has a typed builder function that accepts structured data and returns HTML:

import {
buildQuoteHTML,
buildCOIHTML,
buildNOCHTML,
buildDecPageHTML,
buildEndorsementHTML,
buildSettlementHTML,
buildLossRunHTML,
} from '@openinsure/documents';
const html = buildCOIHTML({
insuredName: 'Acme Roofing LLC',
policyNumber: 'GL-2025-000001',
effectiveDate: '2025-06-01',
expirationDate: '2026-06-01',
insurer: 'Vermont Mutual Insurance',
coverages: [
{ type: 'General Liability', occurrence: 1_000_000, aggregate: 2_000_000 },
{ type: 'Products & Completed Ops', aggregate: 2_000_000 },
],
certificateHolder: {
name: 'Vermont DOT',
address: '219 North Main St, Barre, VT 05641',
},
additionalInsured: true,
producerName: 'Mountain Insurance Agency',
});
const html = buildQuoteHTML({
submission,
ratingAudit, // full factor waterfall from the rating engine
validUntil: '2025-07-01',
producerName: 'Mountain Insurance Agency',
uwName: 'Michael Chen',
});

Documents are rendered to PDF by the doc factory through renderHTMLToPDFWeasy() in apps/api/src/lib/pdf-renderer.ts.

  1. The API route or workflow calls a typed HTML builder from @openinsure/documents
  2. The HTML is posted to the doc factory with print options and any attachments
  3. The doc factory renders the PDF with WeasyPrint
  4. The API stores the PDF in Garage S3 or Cloudflare R2 with SHA-256 audit metadata
import { renderHTMLToPDFWeasy } from '../lib/pdf-renderer';
const pdfBytes = await renderHTMLToPDFWeasy(env, html, {
format: 'Letter',
margin: '0.5in',
});
await storage.put(`policies/${policyId}/dec-page.pdf`, pdfBytes, {
customMetadata: {
source: 'dec-page',
pdfSha256,
htmlSha256,
generatedAt,
},
});

The Forms Library combines uploaded PDF templates with forms that are generated by the doc factory at bind time.

KindSource of truthWorkbench behavior
Stored PDFforms/ and distribution-forms/ objects in document storagePreview and download the stored blank PDF
Generated at BindMHC_FORMS_LIBRARY entries with formType: 'html-rendered'Preview sample HTML through /v1/admin/templates/preview; no blank PDF is downloaded

Generated forms are data-dependent. For example, Form 802 only renders when one or more drivers are excluded, and state UM/PIP forms render only for matching policy states. In the API, generated rows use synthetic keys such as doc-factory://swd-southwind/driver-exclusion-form-802 and expose templatePreviewId when the workbench can show a sample preview.


OpenInsure uses a self-hosted Documenso instance (openinsure-documenso.fly.dev) for e-signature workflows. Documenso is configured via ESIGN_PROVIDER=documenso in wrangler.toml.

  1. Document is generated as a PDF and uploaded to Documenso via the API 2. Signing fields are placed programmatically (producer signature, insured signature, date) 3. Signatories receive an email with a one-click signing link (no account required) 4. On completion, the signed PDF is stored in R2 and the policy/submission record is updated 5. All parties receive a signed copy by email
Terminal window
POST /v1/documents/:id/request-signature
Authorization: Bearer <token>
Content-Type: application/json
{
"documentType": "binder",
"signatories": [
{ "name": "Jane Smith", "email": "jane@acme.com", "role": "insured" },
{ "name": "Mountain Insurance Agency", "email": "agent@mountain.com", "role": "producer" }
],
"redirectUrl": "https://portal.openinsure.dev/policies/{id}?signed=true"
}
Terminal window
GET /v1/documents/:id/signature-status
# Returns: { status: "pending" | "completed" | "declined", signatories: [...] }

Beyond PDFs, the documents package provides tabular exports:

import { exportToCSV, exportToExcel } from '@openinsure/documents';
// Loss run CSV for underwriting
const csv = exportToCSV(lossRunRows, {
columns: ['claimNumber', 'dateOfLoss', 'status', 'totalIncurred'],
filename: 'loss-run-2025.csv',
});
// Bordereaux Excel workbook for carrier submission
const xlsx = await exportToExcel(bordereaux, {
sheets: ['Premium', 'Claims', 'Summary'],
});

Document writes route through resolveDocsStorage(env) in apps/api/src/lib/storage. The provider hierarchy is:

  1. Garage S3 (primary) — self-hosted S3-compatible cluster on Hetzner (mhcmga-storage cpx41 Ashburn + mhcmga-storage-box BX41 Falkenstein). Endpoint: storage.openinsure.dev. Seven buckets provisioned: oi-documents, oi-policy-forms, oi-coi, oi-claims, oi-submissions, oi-rate-manuals, oi-backups.
  2. Cloudflare R2 (fallback) — oi-documents bucket bound as DOCUMENTS in wrangler.toml. Workers fall back to R2 when the Garage endpoint is unavailable.
  3. NoopStorage — test-only fallback used when no storage binding is present.

Uploads, downloads, and deletes all go through this resolver, so product code never hard-codes a provider. Key schema is identical across providers:

policies/{policyId}/dec-page.pdf
policies/{policyId}/endorsements/{endorsementId}.pdf
policies/{policyId}/coi/{coiId}.pdf
claims/{claimId}/settlement-agreement.pdf
submissions/{submissionId}/quote-proposal.pdf

Documents are served through the API at:

GET /v1/documents/{documentId}/download

Presigned URLs with 24-hour expiry are returned for direct download without proxying through the Worker. Presigns work against both Garage and R2.


  1. Create src/builders/{document - type}.ts — returns HTML string 2. Export from src/index.ts
  2. Add a route in apps/api/src/routes/documents.ts that calls the builder and stores the result
  3. Add the document type to the documentType enum in packages/types 5. Write tests in src/__tests__/ using snapshot testing (compare generated HTML to fixture)