DEV Community

SvenNilsson228
SvenNilsson228

Posted on

Password-Reset SMS Alert Service Alternative for Startup Apps (Own the Message)

Short answer: keep the password-reset template, expiry policy, and delivery-state mapping inside the startup app; compare SMS services only after that boundary is fixed, using the actual US and EU sender-registration path, per-message segment count, and receipt-retrieval method for your traffic.

That decision makes the provider replaceable without pretending every provider behaves alike. The application creates a single-use reset URL, renders a short message, submits it through a narrow adapter, and records the provider's opaque message ID. A worker then polls through that same adapter until the message reaches a terminal state or the application's polling budget ends. Registration and billing stay provider-specific. Reset semantics don't.

This is a template-ownership problem first.

Govern the password-reset message as a release artifact

Own the meaning of the message in application code: which event caused it, which variables are allowed, how long the reset token remains valid, and which text version was sent. Let the SMS service own transport details such as sender eligibility in a destination and the raw delivery status. The adapter translates those raw statuses into a deliberately small internal vocabulary.

For a B2B SaaS reset flow, that boundary is more useful than a feature checklist. A provider-hosted template can be convenient when non-engineers must edit transactional copy directly, but it couples the message version and variable contract to that provider. An application-owned template keeps code review, tests, and deployment history together. The catch is that legal review, localization, and copy approval now need an explicit workflow in your repository; a team whose compliance staff must publish copy without an application release should stick with a managed template workflow or build a separate approved-content store.

US and EU traffic should not be collapsed into one imaginary “global SMS” lane. Ask each candidate for the sender-registration steps that apply to your sender type and destinations, then record the answers as deployment prerequisites rather than runtime branches. I'm not sure which service is cheapest for a given startup until its destination mix, registration choices, and encoded segment counts are put into a quote-backed traffic model. A headline per-message number can't resolve that.

Put the contract in code before comparing services

The example below is intentionally provider-free. It renders one compact template, rejects expired reset links before submission, estimates SMS segments for evaluation output, and gives polling a finite budget. The segment estimator distinguishes GSM-7-like content from Unicode and applies the documented single-part and concatenated-message limits: 160/153 characters for GSM-7 and 70/67 for UCS-2. It is conservative because the complete GSM-7 extension-table accounting belongs in a tested library or provider preview, not in a casual helper.

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Protocol


GSM_7_BASIC = frozenset(
    "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ"
    " !\"#¤%&'()*+,-./0123456789:;<=>?"
    "¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ"
    "¿abcdefghijklmnopqrstuvwxyzäöñüà"
)


class DeliveryState(str, Enum):
    ACCEPTED = "accepted"
    DELIVERED = "delivered"
    UNDELIVERED = "undelivered"


@dataclass(frozen=True)
class ResetMessage:
    recipient: str
    reset_url: str
    expires_at: datetime
    template_version: str = "password-reset-v1"

    def render(self, now: datetime) -> str:
        if now >= self.expires_at:
            raise ValueError("reset link has already expired")
        minutes = max(1, int((self.expires_at - now).total_seconds() // 60))
        return f"Reset your password: {self.reset_url} Expires in {minutes} min."


@dataclass(frozen=True)
class Submission:
    message_id: str
    state: DeliveryState


class SmsAdapter(Protocol):
    def send(self, recipient: str, body: str) -> Submission: ...

    def get_delivery(self, message_id: str) -> DeliveryState: ...


def estimated_segments(body: str) -> tuple[str, int]:
    is_gsm_7 = all(character in GSM_7_BASIC for character in body)
    single_limit, joined_limit = (160, 153) if is_gsm_7 else (70, 67)
    units = len(body)
    segments = 1 if units <= single_limit else (units + joined_limit - 1) // joined_limit
    return ("GSM-7" if is_gsm_7 else "UCS-2", segments)


def submit_reset(adapter: SmsAdapter, recipient: str, reset_url: str) -> Submission:
    now = datetime.now(timezone.utc)
    message = ResetMessage(
        recipient=recipient,
        reset_url=reset_url,
        expires_at=now + timedelta(minutes=10),
    )
    body = message.render(now)
    encoding, segments = estimated_segments(body)
    print(
        {
            "event": "password_reset_sms_ready",
            "template_version": message.template_version,
            "encoding": encoding,
            "estimated_segments": segments,
        }
    )
    return adapter.send(recipient=message.recipient, body=body)
Enter fullscreen mode Exit fullscreen mode

The SmsAdapter is the replaceable edge. One implementation can call one provider today and another tomorrow, but neither implementation gets to invent a new template or extend token life. Also, don't log the reset URL or full phone number. The example's event records only template and encoding metadata because observability should not become a second credential leak.

There is a subtle notebook-to-production trap here: a string that looks short can cross a segment boundary after someone adds an emoji or non-GSM character. Run the renderer against representative localized copy in CI, store the resulting encoding and segment count in eval output, and fail the build when an approved budget is exceeded. The estimate is a guardrail, while the provider's own segmentation result remains the billing authority.

Can a startup app compare SMS alert service alternatives by template ownership?

Build the comparison from a small, versioned fixture rather than a marketing page. Include US and EU destinations, the exact password-reset body, every locale you intend to ship, and the sender type the business can actually register. For each candidate, capture quoted cost per submitted segment, required registration work, supported receipt retrieval, retention relevant to polling, and the raw-to-internal status mapping. Keep currency and tax assumptions beside the quote. Prices change; the fixture makes the assumption visible.

Twilio, Amazon SNS, and Vonage are reasonable names to include in a candidate worksheet, but a vendor name is not a result. The winner can change with destination mix and sender eligibility, so this article doesn't rank them. Amazon SES belongs in a different lane: its documentation describes an email service, which makes it a useful reminder that email fallback should remain a separate channel adapter rather than masquerading as SMS.

Decision input Evidence to collect Failure caught early
Template ownership Rendered body, variable schema, version Copy drift and unsafe variables
Message shape Encoding and segment count by locale A “small” edit multiplying segments
Sender readiness Registration approval for each intended route A launch blocked by an unusable sender
Delivery evidence Pollable states and terminal-state definitions Treating acceptance as delivery
Cost model Quote applied to the fixture's segment mix Choosing from an incomparable headline rate

Cheapest and simplest pull in different directions. Owning templates adds a small internal contract and test suite, yet it reduces migration work and makes prompt-like text changes reviewable. A hosted template console may be simpler for a compliance-led publishing process. There isn't one universally correct boundary.

Poll delivery receipts without extending reset authority

Polling is an observation mechanism, not part of the security decision. A reset token should expire according to the application's clock even if the last delivery state is still accepted. Never extend the token merely because a receipt arrived late, and never equate provider acceptance with handset delivery.

Use bounded exponential backoff with jitter, save the provider message ID, and stop on your internal delivered or undelivered state. A worker should also stop after its configured observation window and record receipt_window_ended as an internal operational outcome; that label says the app stopped asking, not that the carrier failed. This distinction keeps dashboards honest.

Keep it boring.

For deployment, review the rendered US and EU fixtures, confirm sender registration before enabling traffic, and run contract tests against each adapter's status mapper. In production, watch submission counts, encoded segments, terminal-state ratios, poll attempts, and template versions without logging reset credentials. Re-evaluate quotes when the country or locale mix changes, and rerun the message eval whenever copy changes. That's the operational loop: render, measure, submit, observe, and expire independently.

References

Top comments (0)