Import / export bundle format
Templates round-trip between Vetrix instances as signed JSON bundles.
The schema is versioned; bundle_version=1 is the current shape.
Envelope
{
"bundle_version": 1,
"exported_at": "2026-04-21T14:00:00Z",
"exported_by": "admin",
"instance_id": "vetrix.example",
"checksum": "<sha256-hex of canonical templates array>",
"templates": [ … ]
}
templates[] entry shape
{
"slug": "issue.assigned",
"category": "notification",
"description": "Sent when an issue is assigned to a user.",
"required_namespaces": ["Issue","Repo","Actor","Recipient"],
"version_note": "",
"subject": "[{{ .Repo.Path }}] You were assigned to #{{ .Issue.Number }}",
"body_plaintext": "…",
"body_html": "",
"locale": "en"
}
Canonical JSON + checksum
The checksum field is the SHA-256 (hex, lowercase) of the
canonical JSON of the templates array. "Canonical" means:
- Templates are sorted by
slugascending. - Each template is emitted as a JSON object with keys in sorted
order (Go's
encoding/jsondoes this formap[string]any). - No whitespace between tokens.
A shell one-liner reproduces the digest:
jq -cS '.templates | map(
{slug,category,description,required_namespaces,version_note,
subject,body_plaintext,body_html,locale}
) | sort_by(.slug)' bundle.json | shasum -a 256
Validation rules
The import endpoint (POST /api/v1/admin/email/bundles/import/dry-run
followed by …/apply) rejects:
bundle_version != 1→409 Conflictwithkind: "version".checksummismatch →400 Bad Requestwithkind: "checksum".- Any slug that doesn't exist in the importing instance's
email_template_definitions→kind: "unknown_slug"per-slug error. Creating new definitions via import is not supported. - A template whose
required_namespacesis not a superset of the local definition's canonical list →kind: "namespace_mismatch". Templates may DECLARE more namespaces than they need, but must never shrink the schema. - Any template whose body fails
engine.Parseorengine.Lint.
Apply semantics
Apply inserts a new email_templates row with status='draft' and
version = MAX(version) + 1 per (definition, locale) for each new
or updated slug. active_version_id is never modified — the
admin must publish explicitly.