Notifications & SMTP
The notification module boots in noop mode by default — verification and password-reset emails are rendered and logged to the backend's stdout rather than delivered. A fresh install therefore needs no mail credentials to be usable, and dev and CI never send anything by accident.
Switching to real delivery
The quickest path is still one relay for everything:
- Open
/admin/modules/notification. - Under Delivery, change Email provider from
nooptosmtp. The SMTP connection fields appear only at this point — they carry a dependency on the provider field, so a default install shows one option instead of six irrelevant ones. - Fill in host, port and TLS mode; username and password only if your relay authenticates.
- Under Sender, set the from-address and display name.
- Save — it takes effect immediately, no restart — and send yourself a message with
POST /v1/notifications/test.
This configures the legacy default sender: until a profile under Sender profiles declares a pattern, these settings are the one profile every message uses (it appears in the delivery log as senderSlug=_legacy), and a stack that sets SMTP_HOST in docker-compose is configured the same way. Adding a profile without patterns changes nothing yet.
The password is stored as a FieldSecret: AES-256-GCM encrypted at rest, never echoed back through the API.
Separating transactional from marketing mail
Reputation is scored per domain and per IP, and vendors enforce the split contractually (MailUp's SMTP+ terms: do not use it for promotional emails). Once a fork sends campaigns, give each workload its own sender:
- Under Sender profiles, add a profile — say Transactional — pick its provider, fill its identity and credentials, and give it the pattern
*. It is now the default for everything. - Add a second profile — Campaigns — on its own domain and relay, with the patterns your campaign categories use:
crm.*,marketing. (A category with no dot, likemarketing, is matched only by the exact pattern;marketing.*would not capture it.) - Optionally pin the access path explicitly: a third profile with
auth.*carries verification, reset and security mail even if someone later edits the default. - Save. The most specific pattern wins, so
auth.verify_emailgoes to theauth.*profile,crm.campaigntocrm.*, and anything else to*. - Prove each profile:
POST /v1/notifications/testwith{"to": "[email protected]", "sender": "<slug>"}. This is the only check that exercises a profile's secret before real mail depends on it.
A profile with no patterns is a draft — it receives nothing and is not validated beyond pattern grammar, so you can prepare a sender before routing traffic to it. Once any profile declares a pattern, exactly one must declare *; saving is refused otherwise (notification.sender_no_default).
Nothing is silently rerouted. If a category matches no profile, or its profile is incomplete, the send fails and the delivery log row says why (sender=campaigns driver=mailup err=not_configured missing=mailup_user). Falling back to the default would push promotional mail through the transactional sender — the thing this feature exists to prevent.
TLS mode
| Mode | Port | When |
|---|---|---|
starttls | 587 | The default, and what nearly every provider wants |
tls | 465 | Implicit TLS, for relays that require it |
none | 25 or 1025 | Local capture only — a dev mail catcher. Never across a network |
Port 25 is blocked outbound by most cloud providers, so starttls on 587 is the right first attempt.
Providers
smtp covers every hosted relay below and any internal MTA — credentials are optional, so an unauthenticated relay on a private network is a first-class setup. mailup talks to MailUp's SMTP+ API directly: create an SMTP+ user in the MailUp console after authorizing a trusted sender, and enter its username (s12345_67) and secret; the notification category is reported to MailUp as the CampaignCode, so their statistics line up with your routing (leave it to MailUp's own default by sending an empty category). A MailUp "success" means accepted, not delivered — the same guarantee an SMTP 250 gives. A Reply-To differing from the from-address has to be enabled on the account by MailUp support.
Every hosted relay maps onto the same SMTP fields. Confirm the host against your provider's current documentation — these are stable but not ours to guarantee.
| Provider | Host | Username | Password |
|---|---|---|---|
| Amazon SES | email-smtp.<region>.amazonaws.com | SES SMTP credentials — generated in the console, not your AWS access key | The matching SMTP secret |
| SendGrid | smtp.sendgrid.net | The literal string apikey | Your API key |
| Postmark | smtp.postmarkapp.com | Server API token | The same token |
| Resend | smtp.resend.com | The literal string resend | Your API key |
Two things trip people up regardless of provider:
- The from-address must be verified with the provider, and for anything beyond a trickle you need domain authentication (SPF and DKIM records). An unverified sender is rejected at the relay, so it looks like a credentials problem when it is a DNS one.
- Amazon SES starts every account in sandbox mode, where you may only send to verified addresses. Requesting production access is a separate step, and until it is granted mail to your users silently fails.
What gets sent
The core base sends transactional mail only, all of it from the auth module: email verification, password reset, suspicious-login and new-device notices, and admin invites.
Transactional mail cannot be opted out of. Preference checks are skipped for it by design — verification and reset messages are required for the product to work. The unsubscribe footer still links to the preferences page, where a user can opt out of marketing categories, with a note that security mail will keep arriving. If your fork adds marketing mail, it must mark it as such or preferences will silently not be honored.
Templates
Templates are seeded into MongoDB on first start, and from then on the database is the source of truth. Edit one at /admin/modules or through PUT /v1/notifications/templates/{templateId}; overriding flips it out of system ownership. Deleting the override restores the shipped default on the next start — that is the undo.
Subjects and plain-text bodies render through Go's text/template; HTML bodies render through html/template, so the HTML path gets contextual escaping. Every send is automatically given an unsubscribe URL, a preferences URL, the app name, and the support email, so a template can use those without the calling module passing them.
When mail does not arrive
Work down this list in order:
- Is the provider still
noop? Then it never left the building — the rendered message is in the backend's stdout. This is the single most common cause. - Check the delivery log at
GET /v1/notifications— filter bycategory,status, orsender. Afailedrow names the profile and the reason (sender=… smtp op=auth code=535,http=401 status=error code=401,err=no_sender_for_category); Orkestra tried and the sender refused. No row at all means the send was never attempted — usually a pre-flight guard found the category's profile unusable, which forauthmail surfaces as a 503 on signup. - Is the address suppressed? A suppression is keyed by address and silently drops delivery. There is no bounce ingestion in the base, so suppressions are added manually — check before assuming a config fault.
- Was it deduplicated? Every send carries an idempotency key, and a repeat within the hour returns the earlier result instead of sending again. Two clicks on "resend" produce one email, on purpose.
- Then look at SPF/DKIM and the provider's own log. By this point the problem is usually at the relay, not in Orkestra.
- With several profiles, check which one carried it.
senderSlugon the row tells you; a category that rode the default when you expectedcrm.*means the pattern is misspelled — a pattern with no dot matches exactly, andcrm.*never matches the barecrm.
:::note Scope Email only. The interface was designed for SMS, push, and webhooks, but none are implemented. Sends are synchronous — there is no queue. No marketing automation, segmentation, or A/B testing; no bounce or complaint ingestion; DKIM signing is the relay's job. :::