DEV Community

rasmusberg6592
rasmusberg6592

Posted on

Password Reset Email Fallback: Isolating Code and Link Template Releases

A password reset email fallback becomes dangerous when one template release can change both the reset link and the fallback code at once. The operational constraint is the release boundary: recovery must keep working while a bad presentation is contained, without minting a second challenge or guessing which message a user received.

Short answer: keep one password-reset challenge, give its link and code presentations separate, immutable template versions, and let the application select exactly one version before dispatch. A managed OTP email API is a reasonable implementation choice only if its template ownership and rollback model preserve that boundary; otherwise, manage the templates in the release system already accountable for recovery policy.

That answer also applies to the less dramatic message next door. In an e-commerce platform, an order receipt is sent after payment settles. Receipt templates and recovery templates may share rendering and transport, but they should not share a release fate. Payment settlement should remain the trigger for a receipt, while a reviewed recovery transition should remain the trigger for a fallback email.

How can a password reset email code and link survive template migration?

Consider a bounded tabletop incident. At 10:00, a shared template bundle publishes an order-receipt wording change and a recovery layout change. At 10:07, synthetic recovery checks show that the code presentation no longer satisfies its rendering contract, while link presentation and receipt dispatch remain within their objectives. The safe response is to stop assigning the affected recovery template version, leave existing challenges valid, and roll forward or back only that presentation. The unsafe response is to replay payment events, issue replacement challenges to every requester, or roll back the whole shared bundle without knowing which messages were already rendered. Those times describe the exercise, not a production claim; the point is to force the ownership boundary into an observable sequence before an incident does it for you.

The invariant is blunt: a template release may change presentation, but it may not create, extend, or switch a recovery challenge.

Now choose from the user's handoff. A reset link fits a flow that starts and finishes in the same browser. An email code fits a flow that routinely crosses devices or needs a value the user can enter into an already-open session. If both are supported, they should be two views of one challenge with one expiry policy, not parallel credentials created by two delivery branches.

Do not send both by default.

The template-ownership question is narrower and more useful than asking which API has the longest feature list. The team that can approve, publish, observe, and roll back recovery copy under the service's recovery SLO should own the releasable template artifact. That may be the application team. It may be a central messaging team when that team also owns localization, preview testing, and urgent rollback. Delegating delivery does not automatically delegate that responsibility, and keeping a template in a repository does not prove that the repository's owners can operate it at 02:00.

Here, migration includes extracting recovery templates from a shared bundle, moving them into a central platform, or bringing them back into the application repository. The direction matters less than preserving version identity while both release paths may have queued work.

Protect challenge secrets across the rendering boundary

The reconstruction starts at challenge selection and follows the blast radius through the order-receipt pipeline.

Delivery telemetry answers whether a message moved through transport. It cannot, by itself, answer which template revision rendered that message, which challenge presentation the application selected, or whether the same user later requested a fallback. Put those identifiers in the internal dispatch record: challenge ID, presentation, immutable template revision, dispatch idempotency key, and a timestamp. Keep secrets and rendered message bodies out of routine logs.

This ordering changes incident response. First identify the application decision and template version. Then inspect rendering. Only then follow the transport outcome. Teams often reverse that order because the delivery dashboard is convenient, but a delivered message can still carry the wrong presentation for the active challenge, and repeatedly sending it does not repair the mismatch.

The receipt path needs the same traceability with a different namespace. An example order ord_7F3A can produce a receipt dispatch keyed to the payment-settlement event and receipt template revision; it must never reuse the recovery challenge's idempotency key or fallback state. The renderer may be shared. The state machines aren't.

Mustache can support a deliberately small rendering boundary. Its syntax manual distinguishes escaped variables from unescaped variables and describes sections based on the value associated with a key. For a recovery template, pass a narrow view model containing the display fields and exactly one recovery presentation. Do not hand the renderer an entire customer, order, payment, or challenge object and hope that template conditionals enforce policy. For an order receipt, use a separate view model with fields such as order number and settled amount; the similar email envelope is not a reason to combine the contracts.

I use one review question here: can on-call name the exact template revision from a dispatch record without opening the email body? If the answer is “we can infer it from deployment time,” ownership is still ambiguous — clock windows are evidence, not identity.

Capacity-plan rollback before assigning template ownership

Once the dispatch can be reconstructed, assign template ownership from rollback authority.

A template revision should be immutable after publication. Promotion assigns a reviewed revision to new dispatches; rollback changes that assignment. Existing dispatch records continue to point to what actually rendered them. This model costs storage and release plumbing, but it removes the worst kind of incident ambiguity: editing a mutable template in place and then discovering that old and new messages have the same nominal version.

The buy-versus-build decision is therefore about who operates that release unit, not who can accept an email address over an API.

Template ownership model Accountable team retains Operational burden Fit
Application-owned artifacts Review, version assignment, rollback, rendering contract Each application maintains release and preview tooling Distinct product language or recovery policy changes often
Central messaging platform Product approval and event contract Platform owns publication, localization primitives, previews, and on-call Several teams can accept one release process and SLO
Managed templates Product policy and integration acceptance tests External control plane becomes part of rollback and audit work Standard flows fit the available version and export controls
Fully self-hosted rendering and transport Every policy, template, queue, and delivery control Largest capacity and on-call surface Hosting boundaries or specialized protocols rule out delegation

Capacity planning belongs in this table even though templates look like static files. Size the publication path for the number of revisions and locales, but size dispatch for the unhappy path: a login spike, repeated recovery requests, a user-triggered switch from link to code, and a template rollback while queued work still references the prior revision. I don't assume one multiplier works across products. The inputs that settle it are the service's own request distribution, retry policy, queue age objective, and retained audit window; without those measurements, a precise peak number would be invented confidence.

An SLO should cover the user-visible recovery outcome and expose component indicators underneath it. Useful internal indicators include challenge creation, selection of a valid template revision, render success, accepted dispatch, and completion before challenge expiry. The receipt pipeline should have its own objective because a settled payment and a locked-out customer have different urgency, retry semantics, and support consequences, even if both eventually call the same mail transport.

Test the template release contract in Go

The preventative code path should make the release boundary boring. This example accepts an already-authorized challenge, selects one immutable template revision, and rejects a view model that tries to carry both presentations. The interfaces are generic; there is no vendor route or SDK to validate.

package recovery

import (
    "context"
    "errors"
    "fmt"
)

type Presentation string

const (
    Link Presentation = "link"
    Code Presentation = "code"
)

type Challenge struct {
    ID           string
    Presentation Presentation
    TemplateRev  string
}

type View struct {
    ResetURL  string
    EmailCode string
}

type Message struct {
    TemplateRevision string
    IdempotencyKey   string
    View             View
}

type Dispatcher interface {
    Dispatch(ctx context.Context, message Message) error
}

func Send(ctx context.Context, dispatcher Dispatcher, challenge Challenge, view View) error {
    if challenge.ID == "" || challenge.TemplateRev == "" {
        return errors.New("challenge and immutable template revision are required")
    }

    switch challenge.Presentation {
    case Link:
        if view.ResetURL == "" || view.EmailCode != "" {
            return errors.New("link presentation requires only a reset URL")
        }
    case Code:
        if view.EmailCode == "" || view.ResetURL != "" {
            return errors.New("code presentation requires only an email code")
        }
    default:
        return fmt.Errorf("unsupported presentation %q", challenge.Presentation)
    }

    return dispatcher.Dispatch(ctx, Message{
        TemplateRevision: challenge.TemplateRev,
        IdempotencyKey:   challenge.ID + ":" + string(challenge.Presentation),
        View:             view,
    })
}
Enter fullscreen mode Exit fullscreen mode

The production store must authorize presentation changes atomically, and expiry should be evaluated by the challenge owner before this function runs. The template registry must also guarantee that TemplateRev belongs to the selected presentation. Those responsibilities are intentionally outside the renderer. The useful property here is visible: the dispatch cannot contain both a reset URL and an email code, while a retry retains the same challenge-derived idempotency key and immutable revision.

Test the branch as a release contract, not just a helper function. Run preview fixtures for every supported locale, verify that required values render, reject unexpected unescaped insertion of user-controlled fields, and exercise assignment rollback with queued dispatches still pointing at the older revision. Then run a recovery synthetic that completes the selected presentation. A green SMTP handoff alone is too shallow an acceptance test.

Small boundary, fewer surprises.

Count the on-call cost of an independent template control plane

Separate release units are not suitable when the organization cannot staff independent publication, preview, and rollback paths; in a small system with one accountable team and one recovery journey, an application-owned template deployed with the service may be easier to reason about. Stick with that simpler unit until independent template changes are frequent enough to justify another control plane.

Managed templates are also a poor fit when their revision, rollback, audit export, or rendering controls cannot satisfy the team's established release process. In that case, keep rendering under the application or internal platform boundary and delegate only transport. The opposite trade-off matters too: fully self-hosting delivery is difficult to justify when standard recovery behavior fits a managed contract and no team is funded to own queues, reputation, retry policy, and continuous transport operations.

I'm not sure there is a universal threshold for centralizing templates. Team topology, locale count, change frequency, and the recovery SLO decide it. What can be decided in advance is the test: one owner must be able to identify a dispatched revision, stop new assignment, preserve existing challenge state, and restore a known-good presentation without replaying unrelated order receipts.

Commercial-message classification is a separate review. The FTC's CAN-SPAM compliance guide explains obligations for commercial email, but legal counsel should classify the actual receipt, recovery, and mixed-content messages. Template reuse does not make those message purposes interchangeable.

The final decision rule is deliberately narrow: keep recovery policy with the challenge owner, place templates with the team that can operate their release lifecycle, and share rendering or transport only across boundaries that preserve version identity. That gives an order receipt and a password reset the tooling they can safely share without forcing them to fail together.

Sources

Top comments (0)