Skip to content

Delivery channels

Recovery delivery is pluggable: CRUDAuth owns the token (mint, one-time-use, redemption); a DeliveryChannel owns the medium and the copy. Email is the built-in channel (EmailConfig); pass channels= to CRUDAuth to add SMS, WhatsApp, push, fired alongside it best-effort. See the email flows guide for usage.

crudauth.email.channel.DeliveryChannel

Bases: ABC

A medium crudauth routes a recovery message over.

crudauth fires every configured channel best-effort and swallows failures per channel, so raise freely on failure (it never surfaces and never stops the next channel). Reliability (retry/queue) belongs inside a channel.

Example
class SMSChannel(DeliveryChannel):
    async def deliver(self, intent: DeliveryIntent, db) -> None:
        if intent.kind != "reset_password" or intent.token is None or db is None:
            return
        user = await db.get(User, intent.user["id"])   # an app column
        if user and user.phone:
            await sms.enqueue(to=user.phone, token=intent.token)  # hand off

deliver abstractmethod async

deliver(
    intent: DeliveryIntent, db: AsyncSession | None
) -> None

Route, render, and send intent.

Raise on failure (crudauth swallows per channel). Must not assume email; read intent.recipient / intent.user.

db is the request-scoped session for the actionable flows (verify / reset / change), or None for the existing_account notice. Use it to load an app column you need (e.g. await db.get(User, intent.user["id"]) for a phone number). It must be used synchronously and never committed or captured for deferred work: it is closed when the request ends, so a queued job that kept it would use a dead session. Read what you need, then enqueue the actual delivery.

crudauth.email.channel.DeliveryIntent dataclass

DeliveryIntent(
    kind: DeliveryKind,
    token: str | None,
    user: dict[str, Any],
    recipient: str,
    expires_in: int,
)

A recovery message crudauth needs delivered.

crudauth owns the token and its lifetime; the channel owns the medium and the copy. Read what you need off recipient / user; do not assume email.

Attributes:

Name Type Description
kind DeliveryKind

Which message this is (verify_email for an email-recovery verify, verify_recovery for any other factor, reset_password / change_email / existing_account). A non-email channel branches on this to pick its own medium-appropriate copy.

token str | None

The signed token, or None for existing_account (a notice with no action).

user dict[str, Any]

The logical-contract user dict (repo.to_dict); empty for the existing_account notice. Contract fields only, so an app column (phone, whatsapp_id, ...) is NOT here - load it off the db handed to deliver.

recipient str

The resolved recovery destination (the email today; for an email change, the NEW address). A non-email channel typically ignores this and loads its own destination off the user.

expires_in int

Token lifetime in seconds (0 when token is None).

crudauth.email.channel.EmailChannel

EmailChannel(config: EmailConfig)

Bases: DeliveryChannel

The built-in channel: renders crudauth's recovery copy and calls the EmailSender.

Behaviorally identical to the email delivery crudauth shipped before delivery was pluggable; the subject/body/link building lives here now.