Skip to content

Sending email from a SCALR app

Every app backend can send transactional email through a shared SMTP provider configured platform-wide. Authentik uses the same SMTP for its recovery (forgot-password) flow, so configuring once lights it up everywhere.

TL;DR

from scalr_email import send

send(
    to="user@example.com",
    subject="Your invoice is ready",
    body="Plain-text body here.",
    body_html="<p>HTML body here.</p>",  # optional
)

Raises NotConfiguredError if SMTP isn't set up. Raises EmailError if the SMTP server rejects the message.

How it's wired

scalr-email is a tiny stdlib-only wrapper around smtplib. It reads the same SMTP_* env vars Authentik consumes, so a single block in .env.shared drives both:

SMTP_HOST=smtp.resend.com
SMTP_PORT=587
SMTP_USERNAME=resend
SMTP_PASSWORD=re_<api-key>
SMTP_FROM=Acme <hello@yourdomain.com>
SMTP_USE_TLS=true

In dev, leaving these blank is fine — apps will boot, but any send() call raises NotConfiguredError. In prod, set them before deploying.

For the production setup walkthrough (provider choice, DNS records, verification), see DEPLOYMENT.md at the repo root → "Set up transactional email."

Adding email to an existing app

If your app's backend was scaffolded after May 2026, scalr-email is already installed and the SMTP env vars are already in your compose file — just from scalr_email import send and go.

For older apps, add the package to your backend Dockerfile:

COPY packages/email-py /packages/email-py
RUN pip install --no-cache-dir -e /packages/email-py

…and forward the SMTP env vars in your docker-compose.yml's backend service environment: block:

SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USERNAME: ${SMTP_USERNAME:-}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
SMTP_FROM: ${SMTP_FROM:-}
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
SMTP_USE_SSL: ${SMTP_USE_SSL:-false}

When this stops being enough

  • Bulk sends. send() is synchronous and blocks the request thread. For more than a few emails per request, move to a background queue (tracked as Tier 2 plan #7).
  • Transactional templates. Right now the API takes raw strings. If you find yourself building HTML strings inline, that's a sign to introduce Jinja templates — scalr-email doesn't ship them intentionally to keep the surface tiny.
  • Per-tenant senders. SMTP_FROM is platform-wide. If you ever need each tenant's mail to come from their own domain, pass sender= per-call (the kwarg already exists) and source it from the tenant's record.

Testing locally

For local development without a real SMTP provider, run a fake SMTP server in a container:

docker run --rm -p 1025:1025 -p 8025:8025 mailhog/mailhog

Then in .env.shared:

SMTP_HOST=host.docker.internal
SMTP_PORT=1025
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM=test@scalr.local
SMTP_USE_TLS=false

MailHog's web UI at http://localhost:8025 shows every email your apps send without actually delivering anything. Useful for working on email content without burning your provider's free tier.