Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions MIGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ Breaking changes and upgrade notes for downstream projects.

---

## mailer: the 15 shared `config/templates/*.html` mails are now body fragments on the default layout (2026-10-04)

#4133: `billing-*`, `org-*`, `referral-reward-earned`, `reset-password-*`, `signup-invite`, `verify-email` and `welcome` are no longer full HTML documents — each is now a body fragment rendered inside the default layout (`_layout/layout.html` + its header/footer/button partials, #4132/#4166), the same mechanism #4164–#4171 already added for a downstream's own templates.

- Each template's old hardcoded sign-off ("The `{{appName}}` Team." / "... Support Team.") is gone — replaced by `brand.signature`, which has **no fallback**: unset, none of the 15 sign off any more.
- Each template's old inline "Please do not reply…" footer is gone, replaced by the shared footer's standardized contact line, which now reaches all 15 (5 of them — the 4 `org-*` mails and `referral-reward-earned` — never rendered a contact line before).
- The 6 CTA templates render their button via `{{> button url=url label="..."}}` instead of a copy-pasted table.
- `appName`/`appContact` no longer come from a caller-passed param — both are now resolved from the brand (#4177). `appContact` is gone from every body (10 of the 15 used to render it inline, now 0 do; it's footer-only, `brand.contact`). `appName` still renders inline in 14 of the 15 bodies (now brand-sourced, `brand.name`, and also used by the header partial); `reset-password-confirm-email` is the one template with none left — its only use was the removed sign-off. Each template's own `<title>` is gone too — 4 of them (`org-member-added`, `referral-reward-earned`, `signup-invite`, `welcome`) had a non-empty one (the other 11 were empty); the layout now has a single `<title>`, always the mail's `subject`.
- Unchanged otherwise: every remaining variable each template renders, its CTA `url`, its "Button not working?" fallback link, and the three mail-specific caveat sentences (reset-password-email, signup-invite, verify-email).

### Action for downstream

Set `config.mailer.brand.signature` (and `brand.logoUrl`/`brand.primaryColor`/etc., #4165) if you want these mails to keep a sign-off or look branded — nothing renders by default. No other action needed: a downstream's own full-document template (source starting with `<!doctype`/`<html>`) is unaffected, it still bypasses the layout entirely.

**If a downstream already edited one of these 15 shared templates in place** (your copy of `config/templates/<name>.html` no longer matches upstream): move your edited copy to `config/templates/<project>/<name>.html`, add one `config.mailer.templates` entry pointing `<name>` at it (`{ templates: { '<name>': 'config/templates/<project>/<name>.html' } }`), then restore the shared file to upstream's version — explicitly (`git checkout devkit-node/master -- config/templates/<name>.html`) before your next `/update-stack`, or let that run's own ISO merge do it (`config/templates/*.html` is ISO-merge scope, same as `modules/`/`lib/`) — in that order, override first, so the ISO merge's overwrite never touches the only copy of your edits. Until that restore happens, your moved copy is still a full HTML document, so it keeps rendering unwrapped (the legacy-full-document path) the whole time — nothing breaks mid-migration, and converting it to a fragment on the shared layout afterward is optional.

---

## skills: thin service re-export, zero-logic unit-test exception, `create-module` `crud-only` option (2026-10-03)

`.claude/skills/{naming,feature,create-module,verify}/SKILL.md` updated — no runtime code changed:
Expand Down Expand Up @@ -111,11 +129,15 @@ rejected send can never break or delay the signup / OAuth redirect response.
**What you will see:** every new signup receives a welcome email once the
mailer is configured. To opt out, set `organizations.welcomeEmail.enabled:
false` in the project config (default `true`, fail-open — read with `?? true`
so an absent key never silently disables it). To customize the copy, edit the
same-named template at `config/templates/welcome.html` (`{{displayName}}`,
`{{appName}}`, `{{url}}`, `{{appContact}}`, and an optional
`{{#if orgName}}` block, omitted in B2C mode). No schema change, no migration
to run.
so an absent key never silently disables it). To customize the copy, don't
edit `config/templates/welcome.html` in place — it's a Devkit-owned shared
template (#4133) and `/update-stack`'s ISO merge will overwrite it. Add a
`config.mailer.templates` entry instead (`{ templates: { welcome:
'config/templates/<project>/welcome.html' } }`) pointing at your own copy,
seeded from the shared one (`{{displayName}}`, `{{appName}}`, `{{url}}`, and
an optional `{{#if orgName}}` block, omitted in B2C mode — `appContact` is no
longer a body variable, it's the brand-driven footer's contact line, #4133).
No schema change, no migration to run.

## Billing: public plans listing now requires a plan tag (2026-09-25)

Expand Down
24 changes: 6 additions & 18 deletions config/templates/billing-credit-exhausted.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
<!doctype html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<p>Hello,</p>
<p>Your <b>{{appName}}</b> account is <b>out of credits</b>.</p>
<p>
Usage is paused until you add more credits or upgrade your plan — nothing is charged automatically.
</p>
<p>Add credits or upgrade your plan on your <a href="{{billingUrl}}">billing dashboard</a>.</p>
<br />
<p>The <b>{{appName}}</b> Team.</p>
<br />
<i style="color: #9b9b9b">Please do not reply to this email, you can contact us <a href="mailto:{{appContact}}">here</a>.</i>
</body>
</html>
<p>Hello,</p>
<p>Your <b>{{appName}}</b> account is <b>out of credits</b>.</p>
<p>
Usage is paused until you add more credits or upgrade your plan — nothing is charged automatically.
</p>
<p>Add credits or upgrade your plan on your <a href="{{billingUrl}}">billing dashboard</a>.</p>
24 changes: 6 additions & 18 deletions config/templates/billing-credit-warning.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
<!doctype html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<p>Hello,</p>
<p>Your <b>{{appName}}</b> account has <b>{{remaining}} credits left</b>.</p>
<p>
Usage keeps working while credits remain. Once they run out, usage is paused until you add credits — nothing is charged automatically.
</p>
<p>Add more credits or upgrade your plan on your <a href="{{billingUrl}}">billing dashboard</a>.</p>
<br />
<p>The <b>{{appName}}</b> Team.</p>
<br />
<i style="color: #9b9b9b">Please do not reply to this email, you can contact us <a href="mailto:{{appContact}}">here</a>.</i>
</body>
</html>
<p>Hello,</p>
<p>Your <b>{{appName}}</b> account has <b>{{remaining}} credits left</b>.</p>
<p>
Usage keeps working while credits remain. Once they run out, usage is paused until you add credits — nothing is charged automatically.
</p>
<p>Add more credits or upgrade your plan on your <a href="{{billingUrl}}">billing dashboard</a>.</p>
20 changes: 4 additions & 16 deletions config/templates/billing-payment-failed.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
<!doctype html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<p>Hello,</p>
<p>We were unable to process the latest payment for your <b>{{appName}}</b> subscription.</p>
<p>Your account remains active for now. Please update your payment method before the grace period ends to avoid any service interruption.</p>
<p>Update your card on your <a href="{{billingPortalUrl}}">billing portal</a>.</p>
<br />
<p>The <b>{{appName}}</b> Team.</p>
<br />
<i style="color: #9b9b9b">Please do not reply to this email, you can contact us <a href="mailto:{{appContact}}">here</a>.</i>
</body>
</html>
<p>Hello,</p>
<p>We were unable to process the latest payment for your <b>{{appName}}</b> subscription.</p>
<p>Your account remains active for now. Please update your payment method before the grace period ends to avoid any service interruption.</p>
<p>Update your card on your <a href="{{billingPortalUrl}}">billing portal</a>.</p>
26 changes: 7 additions & 19 deletions config/templates/billing-quota-reached-100.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<p>Hello,</p>
<p>Your <b>{{appName}}</b> weekly quota is now fully used ({{meterUsed}} / {{meterQuota}} units).</p>
<p>
If you have an extras pack, your usage continues drawing from its remaining balance until it's used up. Otherwise it pauses until you add an
extras pack or upgrade your plan — nothing is charged automatically.
</p>
<p>Check your consumption or add an extras pack on your <a href="{{billingUrl}}">billing dashboard</a>.</p>
<br />
<p>The <b>{{appName}}</b> Team.</p>
<br />
<i style="color: #9b9b9b">Please do not reply to this email, you can contact us <a href="mailto:{{appContact}}">here</a>.</i>
</body>
</html>
<p>Hello,</p>
<p>Your <b>{{appName}}</b> weekly quota is now fully used ({{meterUsed}} / {{meterQuota}} units).</p>
<p>
If you have an extras pack, your usage continues drawing from its remaining balance until it's used up. Otherwise it pauses until you add an
extras pack or upgrade your plan — nothing is charged automatically.
</p>
<p>Check your consumption or add an extras pack on your <a href="{{billingUrl}}">billing dashboard</a>.</p>
26 changes: 7 additions & 19 deletions config/templates/billing-quota-warning-80.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<p>Hello,</p>
<p>Your <b>{{appName}}</b> plan has reached <b>{{threshold}}%</b> of its weekly quota ({{meterUsed}} / {{meterQuota}} units used).</p>
<p>
You still have room to keep going. Once you reach 100%, runs continue only while you have an extras-pack balance; without one they pause until
you add an extras pack or upgrade your plan — nothing is charged automatically.
</p>
<p>Review your usage, add an extras pack, or upgrade your plan on your <a href="{{billingUrl}}">billing dashboard</a>.</p>
<br />
<p>The <b>{{appName}}</b> Team.</p>
<br />
<i style="color: #9b9b9b">Please do not reply to this email, you can contact us <a href="mailto:{{appContact}}">here</a>.</i>
</body>
</html>
<p>Hello,</p>
<p>Your <b>{{appName}}</b> plan has reached <b>{{threshold}}%</b> of its weekly quota ({{meterUsed}} / {{meterQuota}} units used).</p>
<p>
You still have room to keep going. Once you reach 100%, runs continue only while you have an extras-pack balance; without one they pause until
you add an extras pack or upgrade your plan — nothing is charged automatically.
</p>
<p>Review your usage, add an extras pack, or upgrade your plan on your <a href="{{billingUrl}}">billing dashboard</a>.</p>
41 changes: 5 additions & 36 deletions config/templates/org-member-added.html
Original file line number Diff line number Diff line change
@@ -1,36 +1,5 @@
<!doctype html>
<html lang="en">
<head>
<title>{{appName}} invitation</title>
</head>
<body>
<p>Hello {{displayName}},</p>
<p>You have been invited to join <b>{{orgName}}</b> on {{appName}}.</p>
<p>Review and accept the invitation:</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin: 24px 0">
<tr>
<td align="center" bgcolor="#1a1a1a" style="border-radius: 6px">
<a
href="{{url}}"
target="_blank"
rel="noopener noreferrer"
style="
display: inline-block;
padding: 12px 28px;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
font-weight: bold;
line-height: 20px;
color: #ffffff;
text-decoration: none;
border-radius: 6px;
"
>Accept invitation</a
>
</td>
</tr>
</table>
<p style="font-size: 13px; color: #9b9b9b">Button not working? <a href="{{url}}">{{url}}</a></p>
<p>The <b>{{appName}}</b> Team.</p>
</body>
</html>
<p>Hello {{displayName}},</p>
<p>You have been invited to join <b>{{orgName}}</b> on {{appName}}.</p>
<p>Review and accept the invitation:</p>
{{> button url=url label="Accept invitation"}}
<p style="font-size: 13px">Button not working? <a href="{{url}}">{{url}}</a></p>
15 changes: 3 additions & 12 deletions config/templates/org-request-approved.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
<!doctype html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<p>Hello {{displayName}},</p>
<p>Your request to join <b>{{orgName}}</b> on {{appName}} has been approved.</p>
<p>You can now access the organization.</p>
<p>The <b>{{appName}}</b> Team.</p>
</body>
</html>
<p>Hello {{displayName}},</p>
<p>Your request to join <b>{{orgName}}</b> on {{appName}} has been approved.</p>
<p>You can now access the organization.</p>
41 changes: 5 additions & 36 deletions config/templates/org-request-new.html
Original file line number Diff line number Diff line change
@@ -1,36 +1,5 @@
<!doctype html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<p>Hello,</p>
<p><b>{{requesterName}}</b> ({{requesterEmail}}) has requested to join your organization <b>{{orgName}}</b> on {{appName}}.</p>
<p>Review this request in your dashboard:</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin: 24px 0">
<tr>
<td align="center" bgcolor="#1a1a1a" style="border-radius: 6px">
<a
href="{{url}}"
target="_blank"
rel="noopener noreferrer"
style="
display: inline-block;
padding: 12px 28px;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
font-weight: bold;
line-height: 20px;
color: #ffffff;
text-decoration: none;
border-radius: 6px;
"
>Review request</a
>
</td>
</tr>
</table>
<p style="font-size: 13px; color: #9b9b9b">Button not working? <a href="{{url}}">{{url}}</a></p>
<p>The <b>{{appName}}</b> Team.</p>
</body>
</html>
<p>Hello,</p>
<p><b>{{requesterName}}</b> ({{requesterEmail}}) has requested to join your organization <b>{{orgName}}</b> on {{appName}}.</p>
<p>Review this request in your dashboard:</p>
{{> button url=url label="Review request"}}
<p style="font-size: 13px">Button not working? <a href="{{url}}">{{url}}</a></p>
13 changes: 2 additions & 11 deletions config/templates/org-request-rejected.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,2 @@
<!doctype html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<p>Hello {{displayName}},</p>
<p>Your request to join <b>{{orgName}}</b> on {{appName}} has been declined.</p>
<p>The <b>{{appName}}</b> Team.</p>
</body>
</html>
<p>Hello {{displayName}},</p>
<p>Your request to join <b>{{orgName}}</b> on {{appName}} has been declined.</p>
15 changes: 3 additions & 12 deletions config/templates/referral-reward-earned.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
<!doctype html>
<html lang="en">
<head>
<title>{{appName}} referral reward</title>
</head>
<body>
<p>Hello {{displayName}},</p>
<p>Great news — someone you invited just joined {{appName}}, and you've earned a referral reward of {{units}} units.</p>
<p>Thanks for helping grow {{appName}}.</p>
<p>The <b>{{appName}}</b> Team.</p>
</body>
</html>
<p>Hello {{displayName}},</p>
<p>Great news — someone you invited just joined {{appName}}, and you've earned a referral reward of {{units}} units.</p>
<p>Thanks for helping grow {{appName}}.</p>
16 changes: 2 additions & 14 deletions config/templates/reset-password-confirm-email.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,2 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>

<body>
<p>Dear {{displayName}},</p>
<p>This is a confirmation that the password for your account has just been changed</p>
<p>The <b>{{appName}}</b> Support Team.</p>
<br />
<i style="color: #9b9b9b">Please do not reply to this email, you can contact us <a href="mailto:{{appContact}}">here</a>.</i>
</body>
</html>
<p>Dear {{displayName}},</p>
<p>This is a confirmation that the password for your account has just been changed</p>
48 changes: 6 additions & 42 deletions config/templates/reset-password-email.html
Original file line number Diff line number Diff line change
@@ -1,42 +1,6 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>

<body>
<p>Dear {{displayName}},</p>
<p>You have requested to reset your password on {{appName}}</p>
<p>Click the button below to choose a new password:</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin: 24px 0">
<tr>
<td align="center" bgcolor="#1a1a1a" style="border-radius: 6px">
<a
href="{{url}}"
target="_blank"
rel="noopener noreferrer"
style="
display: inline-block;
padding: 12px 28px;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
font-weight: bold;
line-height: 20px;
color: #ffffff;
text-decoration: none;
border-radius: 6px;
"
>Reset password</a
>
</td>
</tr>
</table>
<p style="font-size: 13px; color: #9b9b9b">Button not working? <a href="{{url}}">{{url}}</a></p>
<p>The <b>{{appName}}</b> Support Team.</p>
<br />
<i style="color: #9b9b9b"
>If you didn't make this request, you can ignore this email. Please do not reply to this email, you can contact us
<a href="mailto:{{appContact}}">here</a>.</i
>
</body>
</html>
<p>Dear {{displayName}},</p>
<p>You have requested to reset your password on {{appName}}</p>
<p>Click the button below to choose a new password:</p>
{{> button url=url label="Reset password"}}
<p style="font-size: 13px">Button not working? <a href="{{url}}">{{url}}</a></p>
<p><i>If you didn't make this request, you can ignore this email.</i></p>
Loading
Loading