Email delivery
Memship sends transactional email for address confirmations, password resets, activity confirmations, waitlist promotions, payment receipts, and overdue reminders. Email is disabled until you configure a transport.
There are two ways to configure one, and they are not equivalent — the settings screen wins over .env. Read Which one is in effect before editing anything, especially on an installation that is already running.
| Where | Providers | Best for |
|---|---|---|
| Settings → Integrations → Mailing | Resend, any SMTP server (Gmail is a preset) | Almost everyone. Takes effect on the next request, no restart. |
.env | Resend, any SMTP server | Preseeding credentials at deploy time, and installations older than the settings screen. |
SMTP means a server that accepts STARTTLS, usually on port 587, or no encryption at all. A server reachable only on port 465, which encrypts from the first byte, is not supported.
Option A — the settings screen
Sign in as superadmin and go to Settings → Integrations → Mailing.
The full walkthrough — obtaining the credentials, the DNS records Resend needs, the details of your SMTP server (or a Gmail app password), the Send test button, and a troubleshooting table — is in the Integrations setup guide, section 4. Two points from it are worth repeating here because they are the ones that silently bite:
- Saving credentials does not activate them. The card badge goes to Ready, not Active. You must then set the Active provider selector and save again. Configure → test → activate, in that order. The order is enforced: a provider cannot be activated until a send through its current saved credentials has passed.
- With no active provider, mail is silently skipped — logged, not queued, no error. Nothing in the interface reports a send that never happened.
Each provider card also shows whether its credentials are known to work: Not verified, Working, or Failing with a reason. It is updated by every email sent through that provider, the test included, and it applies to credentials that come from .env too.
Approval and rejection emails are sent the moment the admin decides, so the admin can be told whether they went out. If the provider could not be reached or its sending limit was hit, they are tried again automatically after 1, 5 and 25 minutes, and the approval message says so. Any other failure, such as rejected credentials, is reported straight away and not retried, because it would fail the same way again. The retries run on the worker service, which must be up.
Option B — .env
Set the variables and recreate the backend so it picks them up:
docker compose up -d --force-recreate apiResend:
RESEND_API_KEY=re_your_key_here
RESEND_FROM_EMAIL=noreply@yourdomain.orgSMTP (any server, Gmail included — for Gmail use smtp.gmail.com and an app password):
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_USER=your-username
SMTP_PASSWORD=your-password
SMTP_FROM=noreply@yourdomain.org
SMTP_TLS=trueThe full screen-field → environment-variable mapping is in Integrations setup guide, section 6.
Which one is in effect
Three cases, and the difference matters:
| Situation | What sends |
|---|---|
| The Mailing screen has never been saved | The .env values, exactly as described above: Resend if RESEND_API_KEY is set, otherwise SMTP if SMTP_HOST is set, otherwise nothing. |
| The screen has been saved and an active provider chosen | That provider. Per field, a value saved in the screen wins; a field left empty there falls back to the matching environment variable. |
| The screen has been saved but no active provider chosen | Nothing. Mail is silently skipped, whatever .env says. |
The quick-start stack relies on the first row: its compose file points SMTP_HOST at a local test inbox, so saving the screen replaces the test inbox — with the provider chosen there, or with nothing if none is.
The consequence to watch for: once anyone has saved the Mailing screen, editing .env may have no visible effect — the saved value wins and no error is raised. If you configured Resend in the screen and later change RESEND_API_KEY in .env, the old key keeps sending. Clear the field in the screen to fall back to the environment value.
The screen marks fields it is reading from the environment with a “from environment” badge, which is the quickest way to see which source is live. Port and TLS always have a value, 587 and on, so they carry the badge only when SMTP_PORT or SMTP_TLS is actually set.
Upgrading from a version that offered “Gmail” as a provider: it was always the SMTP transport, and is now called that. Mail keeps going where it went before:
- An installation that set up Gmail in the screen becomes the SMTP provider with Google’s server (
smtp.gmail.com, port 587, STARTTLS) saved explicitly. It does not start followingSMTP_HOST. - An installation configured only through
.envkeeps using itsSMTP_HOST, and is now reported as SMTP rather than Gmail.
Either way, the card reads Not verified until the next email goes through, because its health record belonged to the old provider. It stays active meanwhile.
Verify
Trigger a real send to confirm delivery — for example, request a password reset from the login page, or approve a pending registration and check the approval email arrives. Emails are localized (ES/CA/EN) based on the recipient’s language.
If you configured through the settings screen, prefer its Send test button: it sends through one specific provider even when that provider is not the active one, so you can verify credentials before switching real traffic to them.
Use a member or club admin account for the reset test. Super admins are excluded from that flow by design — their password is reset from the host with python -m app.cli.seed, see Recovering the super admin password — so testing with one looks like mail that never arrives.
What a member can switch off
Each member has a Receive club emails toggle on their profile. It covers the optional templates — booking and activity confirmations, and club announcements — and nothing else:
| Tier | Examples | Member opt-out |
|---|---|---|
| Mandatory | address confirmation, password reset | never applies |
| Operational | receipts, payment reminders, a cancelled booking | never applies |
| Optional | booking and registration confirmations, announcements | applies |
A member who opts out still receives their invoices and the notices that precede a debt, which a club may be obliged to send. Staff can set the toggle on a member’s behalf from the member record, for someone who asks at the desk or has no portal login.
Which templates sit in which tier is Settings → Communications, and the organization’s own switch is checked first: a template switched off there does not send to anybody, whatever the member chose.
Apart from the mandatory mails, templates start switched off, with one exception: a new install starts with the registration approval email on, because an approved applicant has no other way to learn they were let in. It can be switched off like any other. An install created before this default existed keeps whatever it had; if approval emails never arrive, check that switch first. Approving a registration also tells the admin whether the email was sent, switched off, or failed, and why.
Members cannot sign in before email works
Signing in requires a confirmed address, and the only self-service way to confirm one is the link sent at sign-up. So until a provider is configured:
- Self-registration is refused, with a message telling the visitor to contact the club, rather than creating an account whose owner could never use it.
- A member who registered earlier and never confirmed shows as active and cannot sign in. Open their member record: it says the address is unconfirmed and offers Confirm address. That needs
users.write, and it is recorded in the audit log as confirmed by an administrator rather than by the member — confirm only an address you have established is theirs by some other means. - From the host, the same thing without the UI:
python -m app.cli.verify_email --email someone@example.org, or--listto see who is waiting.
A provider that is configured but broken — a wrong API key, say — is not caught by the registration guard: it looks usable and fails at the transport. Those members get stuck the same way, and Confirm address is the way out.
Troubleshooting
- No email at all — check in this order: (1) Settings → Integrations → Mailing, is Active provider set to something other than None? A saved-but-not-activated provider sends nothing. (2) If the screen has never been used, confirm
RESEND_API_KEYorSMTP_HOSTis set in.env; if neither is, email is intentionally disabled. - Changing
.envdid nothing — the Mailing screen has a saved value for that field, and it wins. See Which one is in effect. - Resend rejects the send (403, “domain is not verified”) — the From address is not on a domain verified in Resend, or its DNS records have not propagated. Resend refuses any From address outside a verified domain. The one exception is
onboarding@resend.dev, which needs no domain but delivers only to the address that owns the Resend account — usable for a test, not for real traffic. - Emails delayed — sending is asynchronous via Celery. Check the worker logs:
docker compose logs -f worker. - Links point to the wrong host — set
FRONTEND_URLto your public site URL (see the Configuration reference).