Repository navigation
Self-host email: optional emails opt-in, a mail relay through CoderHelm, and a Self-host admin view - #315
Merged
Conversation
nambok
force-pushed
the
feat/selfhost-email-optin
branch
from
October 26, 2026 21:03
ab14f0a to
a479814
Compare
nambok
force-pushed
the
feat/selfhost-email-optin
branch
from
October 27, 2026 01:13
a479814 to
d2b58d3
Compare
…lm, and a Self-host admin view - Every email goes through one layer (common::mail). On a self-hosted install the optional ones (weekly summary, welcome) are off until an install admin turns them on in Settings, Emails. The hosted service is unchanged. A self-hosted install never emails a coderhelm.com address. - Self-hosted installs may send through CoderHelm's mail relay (hybrid, opt-in, no fallback). It sends templates and parameters only, to listed recipients only, from notify.coderhelm.com with DKIM, SPF and DMARC reject. It has per-install limits, handles bounces and complaints with an automatic pause, and keeps delivery metadata only. - Platform admins see every hybrid install and the relay and email numbers.
nambok
force-pushed
the
feat/selfhost-email-optin
branch
from
October 27, 2026 02:35
d2b58d3 to
30b8de8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Optional email is now opt-in on self-hosted installs; the hosted service is unchanged. An install admin chooses which optional emails go out, and how email leaves the install, in Settings, Emails, with no redeploy. A self-hosted install can also send its email through CoderHelm's new mail relay (part of hybrid) instead of its own Amazon SES. The relay sends only from CoderHelm's own templates, only to the install's listed people, from a separate sending subdomain. Platform admins get a Self-host view of every hybrid install.
Pairs with CoderHelm/coderhelm#231 (dashboard Emails page, Admin Self-host page, install guide).
Every email CoderHelm can send
users.rsplugins.rsknowledge_sync.rsclients/email.rssupport.rssystem.rsweekly_summary.rsauth.rsemail_events.rsThe
run-complete,run-failed,password-changedandpasskey-*SES templates exist but nothing sends them.Changes
Opt-in on self-host
common::mail: one send layer for every email. It checks the deployment (DEPLOYMENT_KIND; anything buthostedcounts as self-hosted), whether optional email is on (SYSTEM/SETTINGS#EMAIL_OPTIONAL, else the config'sOPTIONAL_EMAIL_DEFAULTS), and the route: SES, or the relay. The relay is used only when an admin chose it and the install is registered, and nothing falls back to the other route. A self-hosted install never emails acoderhelm.comaddress by either route.integrations.weeklySummarydefaults to false on self-host and stays true for hosted (split layout). The example config sets false. On self-host the Monday schedule always exists and the worker decides at runtime. Newemail.transport(ses|coderhelm) is the starting value, also a Launch Stack parameter (EmailTransport, defaultses).mailto:unsubscribe on self-host is the install's own inbox, neversupport@coderhelm.com. The worker now getsSUPPORT_INBOX.GET/PUT /api/system/emails(install admins, capabilityemailsin permissions.rs): the optional emails, the essential ones with why, the transport and the relay's status. Switching to the relay lists the install's admins, people and pending invites with it first.optional_emailscheck. It fails when the relay is chosen but unreachable or paused.CoderHelm's mail relay (hosted side of hybrid)
POST /hybrid/email,POST /hybrid/email/recipients,GET /hybrid/email/status. All are signed like every hybrid call, and only for a registered, verified, non-revoked install. The request id is signed and refused on reuse.common::mail::templates). Subjects are fixed. Parameters are typed, length-limited and escaped. Links and addresses in text are removed. Link parameters must point at the install's own verified host. Wording that reads as CoderHelm staff is refused. Secret parameters (signed approval and unsubscribe links) are marked, and nothing stores parameters.notifications@notify.coderhelm.com, named<install address> via CoderHelm, with no Reply-To. A name that looks like CoderHelm, staff or a hosted host falls back to the CloudFront address (thenames::looks_like_coderhelmrule plus digit look-alikes). The footer names the install and its listed admin.lib/mail-relay.ts, hosted only): thenotify.coderhelm.comSES identity with Easy DKIM and MAIL FROMbounce.notify.coderhelm.com, DKIM CNAMEs, MX, SPF-all, DMARCp=rejecton the subdomain, a configuration set with an SNS destination, andses:SendEmaillimited to that identity, that configuration set andses:FromAddress.notifyis added toreserved_names.json.Platform admin: Self-host
GET /api/admin/hybrid/summaryandGET /api/admin/hybrid/:id(with the audit log), andPOST /api/admin/hybrid/:id/email/pause, under theplatform_admincapability in permissions.rs. They need a platform admin and the admin role. Metadata only: recipients appear only as counts.Docs
email.md: the inventory, Settings, Emails, and Send through CoderHelm with what CoderHelm sees.hybrid.md: a trust model row and the Email threat model.options.md: "Weekly summary email (off; turn on in Settings)".config-reference.md,integrations.md.Threat model (adversarial pass)
mail::adversarial::one_install_cannot_sign_as_another_or_replayan_email_dressed_as_coderhelm_support_is_refused_or_reads_as_the_install,the_sender_name_is_the_verified_address_never_staff_wordingan_outside_address_gets_nothinga_phishing_link_never_reaches_the_emailsecret_parameters_never_reach_a_recordManual step for Nam (DNS)
The relay's records are in CDK, in the coderhelm.com Route 53 zone. They only go live when the hosted stack deploys, and the new identity must show Verified in SES before the relay can send. Separately,
_dmarc.coderhelm.comisp=nonetoday and SPF lists only Google. The relay doesn't depend on either because the subdomain has its ownp=reject, but tightening the main domain's DMARC is worth doing.Self-host links (#312) follow-ups
public_urls::support_inbox().pretend_self_hosted(), andhosted_mentions()is empty for every template. The relayed weekly summary is covered too.no_hosted_urls: the updater's default hosted API URL is on the allowlist, with its reason. Infra passesHYBRID_URLthrough the samehybridUrlEnvironment()the services use, so a plain install still carries nothing hosted. The relay's sender (notify.coderhelm.com) appears only in hosted config and tests, so the lint doesn't flag it and needs no entry.param_reserved).blocked).Known limits
link_not_own_host). It should use SES for now.param_reserved), as intended. The self-host support wording is being changed in the self-host links work.Verification
cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace: all pass.cargo test --workspace: 2222 pass, includingno_hosted_urls.infra:npm test, 66 pass (synth for hosted and self-host configs).lambda:python3 -m unittest discover -s tests, 131 pass.selfhost/launch:node --test bootstrap.test.mjs, 25 pass.