Vetrix Docs

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:

  1. Templates are sorted by slug ascending.
  2. Each template is emitted as a JSON object with keys in sorted order (Go's encoding/json does this for map[string]any).
  3. 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 != 1409 Conflict with kind: "version".
  • checksum mismatch → 400 Bad Request with kind: "checksum".
  • Any slug that doesn't exist in the importing instance's email_template_definitionskind: "unknown_slug" per-slug error. Creating new definitions via import is not supported.
  • A template whose required_namespaces is 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.Parse or engine.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.