What — A new user receives one welcome email when their workspace is created.
Why — Today the only emails at signup are transactional (verify, reset); nothing greets the user or points them to the first step.
Scope
- No
welcome template exists in config/templates/ (12 templates). The mailer resolves ./config/templates/<name>.html (lib/helpers/mailer/index.js:87), so a downstream can override the HTML with a same-named file.
- Send point: inside
createOrganizationForUser (modules/organizations/services/organizations.service.js ~60-115), in the if (created) block next to organizationEvents.emit('organization.created', …). It only runs from handleSignupOrganization's provisioning branches: never on convergence, never from the manual "create another org" path (organizations.crud.service.js:135), so it is exactly-once with no dedup field. Do not hook organization.provisioned (fires on convergence) or organization.created (also fires from the manual crud path).
- Fix: add
config/templates/welcome.html; best-effort, non-blocking mailer.sendMail({ template: 'welcome', to, subject, params: { displayName, organizationName, url, appName, appContact } }), behind a toggle (e.g. config.mailer.welcome.enabled, default true).
Edge cases — idempotent by construction; no i18n in the template pipeline today (out of scope).
Tests — new modules/organizations/tests/organizations.service.welcomeEmail.unit.tests.js: sent on created, not on convergence, not when disabled, a mailer failure does not break signup.
Refs: #3747 (generic templates precedent)
Update (scope re-validated)
- Toggle:
config.organizations.welcomeEmail.enabled in the organizations module config (not config.mailer, which is transport only), read with ?? true, commented as fail-open.
- Send point: in
handleSignupOrganization, after buildResult, in both create branches (orgs enabled + disabled) — never on convergence, never on manual org create. A rollback after a send can no longer mail a removed user.
- Fire-and-forget, gated on
mailer.isConfigured(): Promise.resolve(mailer.sendMail(...)).catch(warn) inside a try — never await, never block signup / OAuth redirect.
- Params:
{ displayName, url: getBaseUrl(), appName: config.app.title, appContact: config.app.contact } (+ orgName only when organizations are enabled; add the getBaseUrl import). The template must not require the org name (B2C hides it). Subject Welcome to ${appName}.
- Existing mocks break: patch
organizations.service.signup.unit.tests.js:21-23, organizations.service.silent.catch.unit.tests.js:58-60, organizations.emailVerification.unit.tests.js:23-25, organizations.emailVerification.policy.unit.tests.js:40-42 → sendMail: jest.fn().mockResolvedValue(null).
- Tests also: B2C branch sends without
orgName; sendMail rejecting or returning undefined → signup still succeeds.
- Refs:
createOrganizationForUser is 55-119; with strict email verification the welcome goes out after verification (provisioning runs in verifyEmail).
Scope: validated 2026-09-25
Created via /dev:issue
What — A new user receives one welcome email when their workspace is created.
Why — Today the only emails at signup are transactional (verify, reset); nothing greets the user or points them to the first step.
Scope
welcometemplate exists inconfig/templates/(12 templates). The mailer resolves./config/templates/<name>.html(lib/helpers/mailer/index.js:87), so a downstream can override the HTML with a same-named file.createOrganizationForUser(modules/organizations/services/organizations.service.js~60-115), in theif (created)block next toorganizationEvents.emit('organization.created', …). It only runs fromhandleSignupOrganization's provisioning branches: never on convergence, never from the manual "create another org" path (organizations.crud.service.js:135), so it is exactly-once with no dedup field. Do not hookorganization.provisioned(fires on convergence) ororganization.created(also fires from the manual crud path).config/templates/welcome.html; best-effort, non-blockingmailer.sendMail({ template: 'welcome', to, subject, params: { displayName, organizationName, url, appName, appContact } }), behind a toggle (e.g.config.mailer.welcome.enabled, default true).Edge cases — idempotent by construction; no i18n in the template pipeline today (out of scope).
Tests — new
modules/organizations/tests/organizations.service.welcomeEmail.unit.tests.js: sent oncreated, not on convergence, not when disabled, a mailer failure does not break signup.Refs: #3747 (generic templates precedent)
Update (scope re-validated)
config.organizations.welcomeEmail.enabledin the organizations module config (notconfig.mailer, which is transport only), read with?? true, commented as fail-open.handleSignupOrganization, afterbuildResult, in both create branches (orgs enabled + disabled) — never on convergence, never on manual org create. A rollback after a send can no longer mail a removed user.mailer.isConfigured():Promise.resolve(mailer.sendMail(...)).catch(warn)inside a try — never await, never block signup / OAuth redirect.{ displayName, url: getBaseUrl(), appName: config.app.title, appContact: config.app.contact }(+orgNameonly when organizations are enabled; add thegetBaseUrlimport). The template must not require the org name (B2C hides it). SubjectWelcome to ${appName}.organizations.service.signup.unit.tests.js:21-23,organizations.service.silent.catch.unit.tests.js:58-60,organizations.emailVerification.unit.tests.js:23-25,organizations.emailVerification.policy.unit.tests.js:40-42→sendMail: jest.fn().mockResolvedValue(null).orgName;sendMailrejecting or returning undefined → signup still succeeds.createOrganizationForUseris 55-119; with strict email verification the welcome goes out after verification (provisioning runs inverifyEmail).Scope: validated 2026-09-25
Created via /dev:issue