Back to articles
Templates

Versioning Amazon SES Templates: Patterns That Actually Hold Up in Production

Amazon SES has no first-class versioning — one template name, one current version, no history. Here are the four patterns teams actually use to manage that, with honest trade-offs for each.

The first time most teams hit Amazon SES versioning is when something breaks. A template gets a small edit. Customers see something off. Engineering wants to roll back. They open the SES Console, click into the template, and discover that the version they had ten minutes ago is gone.

Amazon SES has no first-class versioning. There is exactly one current version of a template by name. UpdateTemplate overwrites it. There is no GetTemplateHistory, no RestorePreviousVersion, no diff in the Console. Whatever was there before your last save is gone unless you stored it somewhere yourself.

Teams work around this in four ways. None of them is universally right. The point of this post is to show you what each one actually looks like in production — what it gives you, where it fails, and which combination of patterns matches the way your team actually works.

What the SES API gives you (and what it doesn't)

The relevant surface area in SES is small enough to summarize in one paragraph.

CreateTemplate creates a template by name. UpdateTemplate overwrites it. GetTemplate returns the current version. DeleteTemplate removes it. ListTemplates paginates through names. There is no version field on the resource. There is no audit of who called the API beyond what CloudTrail captured. There is no protection against an UpdateTemplate call that nobody intended to make.

That's it. Whatever versioning behavior your team relies on, you are building on top of those primitives.

Pattern A: Git as source of truth, SES as a deploy target

The most common and most defensible pattern. The canonical version of every template lives in a Git repository. A CI workflow on merge syncs the repo state to SES through UpdateTemplate.

templates/
├── welcome.json
├── password_reset.json
├── trial_expiring.json
└── invoice_paid.json

Each JSON file contains the template name, subject, HTML body, and text body. Pull requests show diffs. Reviews happen the same way they do for code. CI handles deployment.

What this gives you. Every change has an author, a timestamp, a commit message, a reviewer, and a clear before/after. Rollback is git revert plus a re-deploy. You can git blame a line in a template the same way you blame a line of code.

Where it stops scaling. Non-engineers can't reasonably contribute. A marketer who needs to fix a typo in a billing email has to either learn Git, file a ticket, or get given Console access — which puts you back where you started. The other gap is that Git history is commit history, not operational history. It tells you when something was committed, not when it was deployed to which environment, not who pressed the button, not whether the deploy succeeded.

This pattern is the right starting point for almost every engineering team. It stops being enough when copy changes faster than your engineers want to do them.

Pattern B: Suffix-based versioning in SES itself

A second pattern keeps Git out of it and uses the template namespace itself to store versions. Every meaningful change creates a new template name: welcome_v3, welcome_v4. The application code points to the current version.

TEMPLATE_NAME = "welcome_v4"

ses_client.send_templated_email(
    Source="hello@acme.com",
    Destination={"ToAddresses": [user.email]},
    Template=TEMPLATE_NAME,
    TemplateData=json.dumps(template_data),
)

Where this helps. The previous version stays in SES. You can roll back by changing one constant in your application. You can A/B test by routing a fraction of sends to _v4 and the rest to _v3.

Where it hurts. The template namespace gets crowded fast. Five templates with five versions each is twenty-five entries in ListTemplates. You will forget which _v is current. You will accidentally send through welcome_v2 when you meant welcome_v4. Cleanup is a chore nobody schedules.

This pattern is fine as a layer on top of Pattern A — Git holds the canonical, SES holds the immediate previous version for fast rollback. As the only versioning strategy, it falls apart at modest scale.

Pattern C: Blue/green template names with an application-layer pointer

A refinement of Pattern B that solves the namespace clutter. Instead of incrementing a version suffix on every change, you maintain two slots: welcome_blue and welcome_green. The application reads the current pointer from somewhere — a parameter store, a config table, a feature flag — and routes sends accordingly.

current = ssm.get_parameter(Name="/ses/templates/welcome/active")["Parameter"]["Value"]

ses_client.send_templated_email(
    Source="hello@acme.com",
    Destination={"ToAddresses": [user.email]},
    Template=current,  # "welcome_blue" or "welcome_green"
    TemplateData=json.dumps(template_data),
)

Where this helps. Rollback is flipping a pointer. Deployment is updating the inactive slot, then flipping. You get atomic switching. The namespace stays small.

Where it hurts. The application is now doing template routing, which is a responsibility most application code shouldn't have. Caching the pointer correctly matters — you don't want every send to do an SSM lookup. And you've turned a one-parameter operation (UpdateTemplate) into a multi-step deploy that has its own race conditions.

Use this when atomic flips are critical — security-sensitive emails, regulated content, anywhere the cost of even a five-second incorrect-template window is meaningful.

Pattern D: A dedicated control layer

A managed system outside SES that holds version history natively, exposes diffs and rollback through a UI, and pushes the chosen version to SES on publish. SES still does the sending. The dedicated layer holds the history.

What this gives you. Native versioning. Native diff and review. Rollback as a one-click operation. Non-engineers can edit copy without touching Git, the AWS Console, or the CI system. Audit trail at the content level, not just the API-call level.

What it costs. It's another vendor in the path between humans and SES. You need to be confident the access pattern is bounded — read templates, write templates, list versions — and that you can leave cleanly.

This is what Sovy does. Sovy is a control layer for Amazon SES templates. Versions, diffs, rollback, role-based access, audit logs, environment scoping. SES remains your sender of record, your reputation, and the system of truth for delivery. Sovy sits in front of the editing experience, not in front of the email path.

Storing version metadata that matters

Whichever pattern you pick, the version itself isn't enough. You also need metadata that turns "what changed" into "why it changed."

The minimum useful set:

  • Author: the human who made the change, not the deploy machine.
  • Timestamp: when the change was made and when it was deployed (often different).
  • Reason: a free-text field or a linked ticket. "Fix typo" is fine. "" is not.
  • Reviewer: who approved the change before it shipped.
  • Diff: the actual content delta, not just a "1 file changed."
  • Linked deploy: which CI run, which environment, which region.

In Pattern A, this metadata lives across Git, your CI logs, and CloudTrail — and you assemble it when an auditor asks. In Pattern D, it's in one place by design. The work of correlating these signals is real work; budget for it explicitly if you're building it yourself.

Rolling back without a panic

A rollback plan you've never tested isn't a plan. The day you need to roll back a template, you do not want to be reading the SES API docs.

A good rollback procedure has three properties. It is a single command (or a single click). It is reversible — you can roll forward from the rolled-back state. And it is logged, so the next person who looks at the template knows it was rolled back, when, and why.

In Pattern A, the procedure is: git revert <sha>, push, wait for CI. Five minutes if your pipeline is slow. Test it once a quarter on a staging template you don't care about so the muscle memory is there.

In Pattern B, it's editing the template-name constant in a config and redeploying the application. Faster than CI for templates, but it requires an application deploy.

In Pattern C, it's flipping the pointer. Subsecond. The most operationally pleasant of the DIY patterns.

In Pattern D, it's a button. The button is also logged.

Whichever pattern you use, write the runbook. Put it in the same place your incident runbooks live. Test it.

A worked example: shipping a copy change

Consider a one-line change to the welcome email: changing "Get started" to "Start your trial." Here's what each pattern looks like end-to-end.

Pattern A. Edit templates/welcome.json in a branch. Open a PR. Reviewer approves. Merge. CI calls UpdateTemplate. Total elapsed time: 30 minutes if your CI is fast, longer if reviewers are busy. Rollback if needed: git revert, re-merge.

Pattern B. Create a new template welcome_v5 via CreateTemplate. Update the application constant from welcome_v4 to welcome_v5. Deploy the application. Total elapsed time: a full app deploy, which depends on your pipeline. Rollback: change the constant back, deploy again.

Pattern C. Update welcome_green (the inactive slot) via UpdateTemplate. Test in a staging environment by flipping its pointer. Flip the production pointer. Total elapsed time: minutes. Rollback: flip back.

Pattern D. Edit the welcome template in a UI. Save as a new version. Reviewer approves in the same UI. Click publish. Total elapsed time: minutes. Rollback: click "rollback to previous version."

The interesting thing about this comparison isn't which pattern is fastest — it's which one a marketer or PM can use without engineering involvement. For Patterns A through C, the answer is "none of them." For Pattern D, the answer depends on the tool, but the design intent is "yes."

Picking your pattern

Most teams should start with Pattern A. Git already does most of the work. Your team already knows the workflow. The cost is low.

Add Pattern B or Pattern C as an additional layer when fast rollback becomes operationally important. Production incidents involving transactional email are usually time-sensitive — a git revert plus a CI run can be too slow when password resets are going out wrong every second.

Move to Pattern D when one of two things is true: you're spending meaningful engineering time maintaining the metadata, drift detection, and rollback ergonomics that the other patterns don't give you out of the box, or non-engineers legitimately need to edit transactional copy and the cost of routing every change through engineering has become real.

The thing to avoid is pretending that "we use Git, so we have versioning" is a complete answer. Git versions files. Production-grade template management requires versioning the change to SES, with metadata, with a rollback path, and with a paper trail. Those are different problems, and they don't solve themselves.


Sovy is a control layer for Amazon SES templates. It adds version history, diffs, rollback, audit logs, and role-based access on top of SES while staying outside the email delivery path. If you're building these primitives by hand, we'd like to hear from you.