Hugo Fund Formation Platform16 Apr, 05:07 CET

Export Templates

Sponsor-level .docx master templates with fund-level overrides, inherited sponsor details, and a fixed placeholder vocabulary rendered by docxtemplater.

The three export kinds

KindNameUsed for
islIndividual Side LetterPer-LP side letter rendered at commitment time — the LP's own clauses plus MFN-elected clauses.
efElection FormMFN election ballot handed to an LP listing clauses they can elect into.
mslMaster Side LetterFund-wide MSL: the union of all elected / eligible clauses across the fund.

Template inheritance

Each of the three kinds has a sponsor-level master template plus an optional fund-level override. At export time Hugo resolves the template key with a simple precedence chain — no files are copied between rows:

Resolution order (per kind)
  1. Fund-own override — if funds.export_template_{kind}_r2_key is set, use that.
  2. Sponsor master — else if the fund's sponsor has sponsors.export_template_{kind}_r2_key set, use that.
  3. Hardcoded fallback — else fall back to the built-in generateSideLetter path (no template, Hugo composes the document itself).

Implemented by resolveFundTemplateKey(db, fundId, kind) in src/services/sponsor-export-templates.ts. The resolver returns both the R2 key and the source ('fund' | 'sponsor' | 'none') so the UI can show an "Inherits from sponsor" caption when a fund has no override.

Storage layout

Migration 0063 added three nullable R2-key columns to both the sponsors and funds tables:

sponsors.export_template_isl_r2_key
sponsors.export_template_ef_r2_key
sponsors.export_template_msl_r2_key

funds.export_template_isl_r2_key
funds.export_template_ef_r2_key
funds.export_template_msl_r2_key

Each column holds an R2 object key (or NULL). Uploading a new template writes a fresh R2 object and points the column at it; the old object is left in place.

Sponsor details (per-field inheritance)

Migration 0065 added three sponsor-details columns to the sponsors table, mirrored on funds as optional overrides:

ColumnMeaning
firm_nameSponsor law-firm name.
firm_addressMulti-line sponsor address.
firm_countrySponsor country.
Column names stayed firm_* for stability, but the user-visible labels were renamed on 2026-04-13 to Sponsor details / Sponsor name / Sponsor address / Sponsor country since these describe the sponsor law firm, not Hugo's own firm.

Per-field COALESCE

resolveFundFirmDetails(db, fundId) resolves each of the three fields independently:

  1. If the fund has a non-null value for that field → source: 'fund'.
  2. Else if the sponsor has a non-null value → source: 'sponsor'.
  3. Else → source: 'none', value is null.

Resolution is per-field, not per-row: a fund can override firm_address alone and still inherit firm_name and firm_country from the sponsor. Each field's source is tracked independently so the fund-level settings UI can show which values are inherited.

The resolved values are injected into the render data as the {firm_name}, {firm_address}, and {firm_country} placeholders below. {firm_address} preserves newlines via docxtemplater's linebreaks: true mode.

Settings UI

Per-sponsor

/sponsors/:id/export/settings

  • Sponsor-details form (firm_name, firm_address, firm_country) — these are the defaults inherited by every fund in the sponsor.
  • Master-template upload slots for each of isl, ef, msl.
  • Supported-placeholders reference section rendered from the same canonical list.
Per-fund

/funds/:id/export/settings

  • Sponsor-details form with "Inherits from sponsor" captions shown next to any empty override field.
  • Override-template upload slots for each of isl, ef, msl; empty slots display the resolved sponsor master.
  • Same supported-placeholders reference section.

Supported placeholders

The canonical list lives in src/services/document-export-template.ts as the exported SUPPORTED_PLACEHOLDERS and SUPPORTED_LOOPS arrays. Both settings pages render their reference section from that same helper, and the upload-time validator rejects any tag not on the list.

Plain tokens

TokenDescription
{fund_name}Name of the fund.
{lp_name}Investor display name (the investor's `name`).
{lp_legal_name}LP's full legal name as registered.
{lp_short_name}LP's short / 'attn:' name. Empty if not set.
{lp_country}LP's country code (ISO-2). Empty if not set.
{lp_category}Investor category label (e.g. "Pension fund").
{commitment_amount}Commitment formatted with thousands separators.
{commitment_currency}Commitment currency code.
{commitment_date}LP admission date (YYYY-MM-DD), empty if not set.
{today}Today's date in long-form English (e.g. "11 April 2026").
{firm_name}Sponsor name (per-field inheritance: fund override → sponsor default).
{firm_address}Multi-line sponsor address. Newlines preserved via `linebreaks: true`.
{firm_country}Sponsor country, separate from address.

Loop tokens

LoopDescription
{#clauses}...{/clauses}Loop over the selected clauses. Inside: {clause_name}, {text}, {index}.

Inside a {#clauses}...{/clauses} block, the recognized loop-inner tags are {clause_name}, {text}, and {index}. Any other tag inside the loop body is treated as unknown by the validator.

Validation

validateTemplate(buffer) walks every {...} token in the uploaded .docx and buckets them into recognized, unknown, and loop sets. The upload UI shows the result so authors can see exactly which placeholders Hugo will fill and which are typos. Unknown tags don't block upload, but they will surface as docxtemplater render errors at export time — the renderer wraps those into a TemplateRenderError whose offendingTags list feeds back into the UI.

Rendering

At export time Hugo reads the resolved R2 object, hands the buffer to renderIslFromTemplate(buffer, data), and streams the resulting .docx back to the client. The render data is built by buildRenderData(ExportData) and contains exactly the fields enumerated in the placeholder table above — no dynamic expansion, no template-defined helpers. Rendering is pure JS (pizzip + docxtemplater), synchronous, and runs inside the Workers runtime with no native deps.

When a fund has no template for a given kind (neither fund override nor sponsor master), exportIslHandler falls back to the hardcoded generateSideLetter path. The hardcoded path does not use the placeholder vocabulary — it composes the document programmatically.
Ctrl+K to open · ↑↓ navigate · Enter go · Esc close
Copied