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.

Attributes:

Name Type Description
sends_email bool

Whether this channel emails intent.recipient. Only such channels receive change_email, and change-email mounts only when one is configured. EmailChannel sets it; set it on a custom channel that emails the recipient.

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 verify / reset / change and the email_changed notice, 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, or the existing_account / email_changed notices). A non-email channel branches on this to pick its own medium-appropriate copy.

token str | None

The signed token, or None for a notice (existing_account, email_changed), which has 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

Where the message is addressed. For verify, reset and existing_account it's the recovery factor's value (an email address for email recovery, a phone number for phone recovery). For change_email it's the NEW email address, and only channels with sends_email receive that kind, since its token must reach that address and nothing else. For email_changed it's the previous email address.

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.