Concepts · Templates
MCTF format
Matter Canonical Template Format — the seven-file directory shape every template revision carries. Strict-validated MDX body inside a sandboxed JSX allowlist, JSON Schema field manifest with PII classification, three-position clause grammar, regulatory-regime-tuple signer manifest.
Last updated
A template revision is a directory of seven JSON / MDX files plus the body. Every file's shape is strict-validated by Zod at the boundary so malicious or malformed upstream inputs are rejected before they reach any consumer.
trv_<sha256-prefix>/
├── manifest.json # Root descriptor; SHA-256 content-address.
├── body.mdx # MDX body inside the sandbox allowlist.
├── fields.schema.json # JSON Schema for parameters; PII classification per field.
├── clauses.json # Three-position grammar per negotiable clause.
├── signers.json # Signer-role manifest with regulatory-regime tuples.
├── attachments.json # Declarative attaches_to / attaches_from grammar.
├── jurisdiction_scope.json # Per-jurisdiction (ISO-3166-2) override map.
├── attribution.json # Non-removable attribution payload (CC BY 4.0 etc.).
├── render_capabilities.json
└── source.json # Upstream-repo provenance with commit_sha pin.The MCTF directory is committed to git under
packages/templates/templates/<source>/<template_id>/ and snapshotted to
the object-lock archive store on every merge to main, so a force-push
to the repo cannot erase the canonical authoring source.
manifest.json
{
"template_id": "matter:initial_board_resolution:2026-06-01",
"revision_id": "trv_a1b2c3d4e5f6",
"version": "2026-06-01",
"sha256": "...",
"name": "Initial board resolutions (Day-0 organizational consent)",
"kind": "initial_board_resolution",
"category": "governance",
"availability": "ga",
"visibility": "platform",
"publisher_org_id": null,
"parent_revision_id": null,
"lifecycle_phases": ["create"],
"body_format": "mdx",
"body_engine": "handlebars",
"body_sha256": "..."
}body_engine is pinned to ["handlebars"] in v1. A future engine
addition requires both a schema migration AND a renderer change —
smuggled engines from third-party MCTF sources cannot get past
validation. version is regex-validated YYYY-MM-DD(-<suffix>)?;
string compare is sort-equivalent to chronological order so overlay
compatible_revisions ranges work via simple comparison.
body.mdx (sandbox-locked)
The body is MDX with {{var}} Handlebars placeholders. Three sandbox
layers run before render:
-
SSTI pre-pass. Linear-time scan over the body for the canonical prototype-pollution tokens (
constructor,__proto__,prototype,caller,callee). Case-insensitive —{{Constructor}}is caught. Bounded at 50ms vianode:vm. -
MDX AST allowlist. Parsed via
@mdx-js/mdx; the walker rejects:- Any JSX element outside the four allowed:
<SignatureBlock>,<Exhibit>,<DefinedTerm>,<Attribution>. - Every
mdxFlowExpression/mdxTextExpressionnode — MDX expressions are not the substitution surface; only Handlebars{{var}}placeholders are. This closes the{(function(){return process.env.DATABASE_URL})()}exfiltration class. - Every
mdxjsEsm(top-levelimport/export). - Anything else not in the standard-markdown allowlist.
- Any JSX element outside the four allowed:
-
Handlebars compile + fill. The compile pass runs under a 1-second budget against a frozen helper registry (
Object.freeze(handlebars.helpers)). Any helper registered after freeze throws in strict mode.
fields.schema.json
Standard JSON Schema (Draft 2020-12) with strict invariants:
-
type: "object",additionalProperties: false— caller cannot smuggle extra parameters. -
Every property carries
x-matter-classification∈public | internal | confidential | restricted. Defaults: any field with no annotation defaults toconfidential. Free-text fields (type: string, nopattern, noenum) default torestrictedregardless of declared annotation. Authors must explicitly downgrade withx-matter-explainer.downgrade_justification. -
x-matter-pii-kind∈ssn | ein | phone | email | home_address | dob | bank_account | passport | ip_address | person_name | ein_or_tax_id | free_text | other. -
x-matter-redact-in-webhook: truereplaces the field's value with"<redacted:${pii_kind}>"in outbound webhook payloads. -
x-matter-encrypt-at-rest: truewraps the value in a per-data-subject AES-256-GCM envelope (KMS-keyed) so GDPR Article 17 erasure deletes the key without breaking the S3 Compliance Lock on the archive bundle.
clauses.json
The three-position grammar:
[
{
"id": "vesting_acceleration_on_acquisition",
"label": "Acceleration on acquisition",
"category": "founder_protection",
"negotiation_eligible": true,
"standard_position": { "value": "single_trigger_25_percent" },
"fallbacks": [
{ "value": "double_trigger_full" },
{ "value": "no_acceleration" }
],
"unacceptable_deviations": [
{ "value": "single_trigger_100_percent" }
],
"guidance_notes": "Trigger threshold typically lands at 50% of remaining unvested shares.",
"depends_on": null
}
]Overlays propose { clause_id, selected_value, justification }. The
compose layer validates selected_value against permittedSelections
(standard + fallbacks); values in unacceptable_deviations are rejected
with 422 deviation_not_permitted.
signers.json
Each role declares allowed signature methods, regulatory-regime tuples, counterpart support, and witness requirements:
[
{
"role": "sole_director",
"capacity": "director",
"count": { "min": 1, "max": 1 },
"counterparts_allowed": false,
"witnesses_required": { "count": 0, "capacity": null },
"method_allowed": ["esign", "wet_signature"],
"method_forbidden_in_live": ["agent_authorized"],
"legal_bases": [
{ "regulatory_regime": "esign_ueta_us", "legal_basis_within_regime": "ueta_electronic_agent" },
{ "regulatory_regime": "esign_ueta_us", "legal_basis_within_regime": "esra_consent" },
{ "regulatory_regime": "wet_witness", "legal_basis_within_regime": "wet_signature_witness" }
],
"semantic_tag": "incorporator_or_initial_director"
}
]regulatory_regime reserves slots for eidas_eu and eta_sg ahead of
the M6 EU + Singapore rollout. The wet_signature_witness validator
requires witnesses_required.count >= 1 — MCTF validator fails CI
otherwise.
jurisdiction_scope.json
{
"applies_in": ["US-DE", "US-CA"],
"required": [],
"variants": {
"US-CA": { "jurisdiction_variant_id": "jvr_california_specific" }
}
}ISO-3166-2 codes only. The render pipeline applies the
JurisdictionVariant at compose-step 6 (before any org overlay), so
jurisdiction-specific clause adjustments don't require an org to
re-author the same overlay for every California entity.
render_capabilities.json
{
"needs_complex_layout": false,
"needs_pixel_perfect_typography": false,
"needs_form_fields": false
}The capability router maps to engines — react-pdf for simple bodies,
Playwright for templates that need pixel-perfect typography or
interactive form fields. The engine selection is observability-tagged
via RoutingDecision.reason so dashboards can correlate engine choice
with latency / failure rate.
See also
- Customization — overlay grammar walkthrough.
- Render and sign — end-to-end render flow.
- Sources — auto-generated catalog of source provenance.