DEV Community

ValenciaMoss6824
ValenciaMoss6824

Posted on

S3 Retention for 12 Locale SaaS Signup Password Reset Email API and SMS OTP

Short answer: use an application-owned email template and a single-use link for B2B SaaS signup verification and password reset; add SMS OTP only for accounts with a previously verified phone number and a recovery policy that justifies operating a second credential format. Email is usually the simpler starting point here, but “cheaper” cannot be decided from a per-message quote. Measure attempts, SMS segments, support work, and retained evidence in the US and EU before calling either channel cheaper.

The bill is made of delivery attempts, SMS segments, retries, template releases, support handling, and storage for the evidence retained after each attempt. The dominant term must come from the product's own event counts. One term can still be bounded before a vendor is chosen: an SMS encoded as GSM-7 allows 160 characters in a single message or 153 per segment when concatenated, while UCS-2 allows 70 or 67. A locale change can therefore alter the number of billable segments even when the authentication logic is untouched. Shortening a template enough to stay within its applicable single-message limit moves that term; deleting raw message bodies and credentials after processing moves the retention term.

Start with evidence, not transport.

Failure modes in retained signup verification evidence

Template ownership matters because a verification message is part of a security decision. For application-owned templates, the repository can bind purpose, locale, expiry wording, and a version to the same review that changes token behavior. For delivery-system-owned templates, a separate publishing surface owns the content, so the application must record the exact remote version used. Both models can work. The hard requirement is that an investigator can identify what the system intended to send without recovering the credential itself.

This is where an object-storage mindset helps. “Keep everything” sounds cautious until access control, deletion, replication, and incident scope enter the discussion. A useful evidence object contains a pseudonymous account reference, purpose, channel, locale, template version, attempt identifier, encoding class and segment count for SMS, timestamps, and outcome state. It does not contain the password-reset link, OTP, rendered body, email address, or phone number. Those secrets are valuable during delivery and dangerous afterward.

The retention design should answer three separate questions: what is needed to reject an old credential, what is needed to explain a template release, and what is needed to diagnose a delivery funnel. Those datasets don't need identical lifetimes. Credential validity belongs in the transactional authentication state. Template provenance belongs with the release record. Aggregated delivery outcomes can outlive per-recipient evidence when the organization no longer needs the latter. Mixing all three into one permanent event object makes deletion harder and tells operators less than they expect.

I don't trust a storage policy that begins with a duration and works backward. Begin with the investigation question, name the fields that answer it, assign an owner, and only then choose a retention period under the organization's legal and security requirements. I'm not sure what period is appropriate for a particular US or EU deployment; the answer depends on obligations and response procedures that a channel comparison cannot establish.

Twelve locales make the ownership boundary visible. An application-owned release can render every locale from one immutable revision before deployment. A remotely owned release can move independently, which is valuable when content or legal teams must publish without an application deployment, but the remote revision has to be captured on the attempt. The catch is that application ownership is not suitable when urgent, audited wording changes routinely need a separate operator workflow. Stick with delivery-system ownership in that case, and make its version identifier a required input rather than an optional log field.

Question Application-owned template Delivery-system-owned template
Who releases wording? The application release workflow A separate publishing workflow
What identifies content? Repository revision plus template version Remote immutable template version
How are locales tested? Render the release candidate together Test the published contract before use
What complicates migration? Rendered payload and adapter contract Template identifiers and variable contracts
When is it a poor fit? Independent urgent publishing is required Exact remote versions cannot be retained

That table is a control map, not a winner. Template ownership decides where change authority lives; the channel decides how the credential reaches a destination. Treating those as one choice hides the person who can alter security wording.

How does SaaS login recovery change between password reset email API and SMS OTP?

Compare complete recovery paths, not successful API calls. The email path needs a controlled sending domain, a purpose-bound link, template rendering, delivery evidence, and a completion transition. DKIM defines a way for a signing domain to take responsibility for a message through a cryptographic signature whose verification uses published key material. That supports domain-level message authentication; it does not establish that a particular person still controls the mailbox or that the message reached an inbox.

The SMS path needs a phone number verified before recovery, an OTP bound to a purpose, encoding and segment accounting, delivery evidence, and the same kind of completion transition. A phone number supplied during recovery isn't an acceptable recovery factor because the requester would be choosing the destination. Also, visible character count is not enough for estimating SMS shape — GSM-7 and UCS-2 have different limits, and concatenated messages reserve characters for segmentation.

For a B2B SaaS account that already verifies an email address during signup, email reuses the established destination and avoids operating another enrollment lifecycle. That is why it is the least complex default, not because email is universally more reliable or secure. SMS OTP is a defensible addition when a phone was independently verified earlier, mailbox loss is an important observed failure mode, and the team accepts a second template, credential, abuse, and evidence path. If the user population cannot reliably access email but already has governed phone enrollment, the choice can reverse.

No channel fixes a weak state machine. Return the same outward response for known and unknown accounts, keep credentials short-lived and single-use, and make a newer issue supersede the relevant older one. Delivery acceptance and recovery completion are separate events. A delayed first message must not regain authority after a second credential has been issued, and a retry must not silently mint another credential.

The useful cost denominator is completed, legitimate recoveries. Let starts be authorized recovery starts, attempts the average delivery attempts per start, segments the measured SMS segments per attempt, and support_minutes the handling time per start. Add the recurring cost of template publication and evidence controls. Use separate inputs for US and EU traffic because combining regions before measurement can conceal different destination, locale, and support mixes. Don't publish an illustrative result as if it were production evidence.

from dataclasses import dataclass
from decimal import Decimal


@dataclass(frozen=True)
class RecoveryCost:
    starts: int
    attempts_per_start: Decimal
    delivery_units_per_attempt: Decimal
    cost_per_delivery_unit: Decimal
    support_minutes_per_start: Decimal
    cost_per_support_minute: Decimal
    template_and_evidence_cost: Decimal

    def total(self) -> Decimal:
        delivery = (
            Decimal(self.starts)
            * self.attempts_per_start
            * self.delivery_units_per_attempt
            * self.cost_per_delivery_unit
        )
        support = (
            Decimal(self.starts)
            * self.support_minutes_per_start
            * self.cost_per_support_minute
        )
        return delivery + support + self.template_and_evidence_cost

    def per_completed_recovery(self, completed: int) -> Decimal:
        if completed <= 0:
            raise ValueError("completed recoveries must be positive")
        return self.total() / Decimal(completed)
Enter fullscreen mode Exit fullscreen mode

For email, set delivery_units_per_attempt to the unit used by the actual contract rather than borrowing the SMS model. For SMS, populate it with observed encoded segments. The model is deliberately boring. It prevents a low attempt price from masking retries, extra segments, or support work, and it keeps invented rate-card numbers out of an architecture decision.

Template governance starts at release

A release candidate should render all 12 locales with the longest permitted variables, verify that purpose and expiry wording are present, extract exactly one expected email link, classify SMS encoding, and record the resulting segment count. The same fixture should cover signup verification and password reset as different purposes even if they share layout fragments. A signup link proves control of an address for signup; a reset credential authorizes a recovery transition. Similar prose does not make those states interchangeable.

Use a narrow command at the application boundary. Eligibility and credential creation happen before rendering; the transport receives an already authorized command. Persisted evidence identifies the attempt and template, while the credential stays in short-lived processing memory. This split keeps a mail or SMS adapter from deciding account state and keeps raw credentials out of general logs.

from dataclasses import dataclass
from datetime import datetime
from typing import Literal, Protocol


Channel = Literal["email", "sms"]
Purpose = Literal["signup_verification", "password_reset"]


@dataclass(frozen=True)
class DeliveryCommand:
    destination_ref: str
    channel: Channel
    purpose: Purpose
    locale: str
    template_version: str
    credential: str


@dataclass(frozen=True)
class DeliveryEvidence:
    attempt_id: str
    channel: Channel
    purpose: Purpose
    locale: str
    template_version: str
    accepted_at: datetime
    sms_encoding: str | None = None
    sms_segments: int | None = None


class DeliveryAdapter(Protocol):
    def send(self, command: DeliveryCommand) -> DeliveryEvidence: ...
Enter fullscreen mode Exit fullscreen mode

Small boundary. Clear responsibility.

The tests need awkward orderings rather than a single happy-path response: enqueue one template version and publish another before processing; issue two credentials and deliver the older one last; retry an accepted attempt; expire a credential before delivery; and render the maximum allowed variables in each locale. Assertions belong at both boundaries. Rendering tests prove the released artifact, while state-transition tests prove that only the current credential can complete its intended purpose.

DKIM configuration deserves a separate deployment check from template content. The signature mechanism concerns the signing domain and published key material; the template test concerns rendered purpose, variables, locale, and version. Combining them into “email worked” loses the distinction needed when one changes without the other.

The retention decision closes the investigation window

After the credential expires, remove the raw link or OTP from queues, traces, and temporary processing stores according to the defined lifecycle. After recipient-level evidence no longer serves an approved security, support, or legal purpose, delete it or reduce it to aggregate outcomes. Keep immutable template releases only as long as they support the stated change-control need, with access restricted to the people and systems that perform that work.

This is a deliberate loss of detail.

If an investigation begins after recipient-level evidence has expired, the team may be able to establish aggregate failure rates and the deployed template version but not reconstruct the exact attempt history for one account. Longer retention buys a wider investigation window while increasing storage, access-control, deletion, and exposure obligations. Shorter retention reduces those obligations while narrowing forensic reach. There is no honest universal duration; document who accepts that trade-off and test deletion with the same seriousness as delivery.

The final decision rule is compact: choose application-owned password-reset email when signup already establishes the mailbox and security wording can follow the application release; choose delivery-system ownership when independent, audited publishing is a real organizational requirement; add SMS OTP only when a previously verified phone solves an observed recovery gap. Recalculate cost per completed recovery from regional event data after each material template or retry change. Then stop retaining message bodies and credentials, accepting that an incident outside the evidence window will have fewer account-level details.

References

Top comments (0)