Exact HTTPS hosts copy-stored CTAs may link to. Injected by the composition root.
Email send core (recipient dedupe → template resolution → render → SendGrid). Injected for sendTest — admin test sends go through the same pipeline as the lambda's event-driven sends.
Discard a draft: clear draft_version_id. The version row remains — it is
immutable history. live_version_id is untouched, so a variant with a live
version keeps sending.
Authoring list/read, cached under the admin key-prefix partition. Projects
invalidReason from the validator (collect mode), so a broken variant
shows its problem in the admin list without a write or render. Write/
render validation stays throwing (update re-validates).
Optionalids?: readonly string[]Optionalkeys?: readonly (Optionallive?: booleanOnly variants with a live version.
Optionalpage?: numberOptionalpageSize?: numberOptionalstatusVariants?: readonly string[]A variant's full authoring history, newest first — what the editor's Versions tab lists and diffs (EMAIL_TEMPLATES.md §4.6: "field-level diff between any two versions to make rollback informed"). versions stays the two-pointer read for the working copy; this is the append-only trail behind it.
Uncached, like versions: the tab renders straight after a save, and version rows are immutable so there is nothing to invalidate anyway.
Send a test email via EmailSendCore, exercising the full send pipeline (recipient dedupe → active template resolution → render → SendGrid) in the same way the lambda does for event-driven sends. One implementation, two entrypoints.
A completed send writes emailTemplate.testSendRequested carrying the
exact copy exercised (version + content hash — EMAIL_TEMPLATES.md §4.6);
the skip outcomes are returned un-audited so the caller can tell the
operator why nothing arrived. Throws on SendGrid failure, before any
activity row exists.
Save + lifecycle (PATCH semantics, §4.5). Content fields validate (mode
throw) and insert a version row; a status change moves the pointers,
running the two-actor guard on ACTIVE. Both happen in one transaction
with one audit row; every write then busts the shared cache.
The full live-vs-draft content pair for a variant, plus the pointers' copy.
The authoring list flattens to the working copy (EmailTemplate) so the
versions tab diffs live against draft here instead.
Authoring and lifecycle for stored email templates (docs/EMAIL_TEMPLATES.md §4.5).
updatespans both tables in one transaction — content writes an immutable version row (the trigger snapsdraft_version_id), a status change moves the pointers on the identity row — and every write commits anadmin.dashboard_user_activityrow in the same transaction.The two-actor activation control lives here:
status: "ACTIVE"is rejected when the publishing actor is also thelastEditedByof the copy being published (assertSecondActor).status: "DISABLED"is deliberately single-actor — it is the kill switch, reviewed after the fact (§4.6).Receives the shared EmailTemplateCache as an injected dependency and reads its authoring projection through the
adminpartition. Every write busts both surfaces via EmailTemplateCache.invalidate — the shared template-key tag vocabulary evicts this surface's stale pages AND the send surface's pages through the one namespace.