auth-transactional-emails
Why It Exists
This skill is the template-authoring half of a pattern split for a concrete
reason, not a stylistic one: adr/0001-skills-not-dashboard-module.md (from
the docs/sessions/2026-08-12-auth-email-skills/ dossier) rejected an
earlier plan to bake the whole SES + Terraform + MJML/Supabase-mailer pattern
into templates/dashboard's auth module and consume @cogs/* packages
directly. Two of those four target packages — @cogs/send-service and
@cogs/supabase-sync — are private: true and unpublished, so a
dashboard-module version built as originally scoped would have shipped
something that silently didn't work for anyone outside the @cogs family.
The ADR chose two standalone skills instead, split by lifecycle: this one
owns template authoring and its own sync-templates.mjs; the sibling
auth-email-provisioning owns infra and never touches template content.
The gap this skill documents most pointedly, though, traces to a real
production incident, not a hypothetical. The follow-up commit 5c5ba1e
("document captcha rollback trap + unpushed-templates gap in auth-email
skills") folds back two incidents from the jobweave rollout, one of which
belongs squarely to this skill: compiled templates can sit unpushed
indefinitely with zero errors if sync-templates.mjs --apply never actually
runs, so real users silently keep getting Supabase's stock default
templates the whole time. The pipeline existing — compile-templates.mjs
and sync-templates.mjs present in the repo, even run once during
scaffolding — is not evidence it ever ran against the live project. That
observed failure mode is why the skill's own verification advice insists on
a live Management API GET of mailer_templates_*_content, not a check that
the scripts exist.
The --emails cogs|local split (introduced in the same originating commit,
d5ddcfb, "feat(skills): add auth-email-provisioning + auth-transactional-
emails") exists because the upstream @cogs/emails package this skill's
render core is ported from has a real, unfixed gap: an unrecognized brand
slug silently falls back to Workloom branding (brands/index.ts:55) instead
of erroring, which can ship the wrong brand's logo/colors/copy into a real
user's inbox with no error anywhere in the pipeline. This skill's own
local fork of the render core deliberately corrects that to fail closed —
but only for consumers who choose --emails local; depending on the
published @cogs/emails package as-is inherits the fail-open behavior
unchanged, since it's an upstream gap this skill's code can't patch from the
outside.
What It Does
auth-transactional-emails owns MJML/EJS template source, brand-token
theming, and the compiled artifact that carries rendered template content
into Supabase's mailer. compile-templates.mjs renders the 8
Supabase-native templates (confirmation, recovery, magic_link,
invite, email_change, email_changed_notification,
password_changed_notification, reauthentication) with no runtime
values, so Supabase's own Go placeholders ({{ .TokenHash }},
{{ .SiteURL }}, etc.) survive untouched into a versioned
compiled-templates.json artifact. sync-templates.mjs is the sole owner
of pushing that artifact's content into Supabase's mailer config via the
Management API, validating the full 8-key schema and issuing at most one
PATCH — never SMTP or captcha config, which belongs exclusively to the
sibling skill's sync-auth-config.mjs.
One template, new_device_login, is deliberately excluded from that
pipeline. It's genuinely out-of-band: rendered at request time with real
runtime values (device, location, ip, a real link) and sent by a standalone
Hono send-service, authenticated by a fully specified v2-timestamped HMAC
contract (x-cogs-signature/x-cogs-timestamp, 5-minute tolerance,
constant-time compare, multi-secret key rotation). Confusing the two paths —
trying to push new_device_login through sync-templates.mjs, or expecting
the send-service to handle the other 8 — is the mistake this skill's lead
invariant exists to prevent.
How To Use It
Triggers on: "MJML email templates", "Supabase mailer template sync", "new_device_login HMAC", "compiled-templates.json", "sync-templates.mjs", "--emails cogs or local", "brand tokens for transactional email", "getBrandTokens fail closed", or authoring/compiling/syncing the branded auth-email template half of this pattern.
skills add git@github.com:catesandrew/next-starters.git --skill skills/auth-transactional-emails -g
npm install @next-starters/skill-auth-transactional-emails
/plugin marketplace add catesandrew/next-starters
/plugin install auth-transactional-emails@next-starters
Gotchas & Invariants
- The Supabase-mailer-sync vs. out-of-band split is the single most
important fact in this skill: exactly 8 templates get PATCHed into
Supabase's own mailer config, and exactly one (
new_device_login) is rendered and sent separately by the send-service.compiled-templates.jsonmust never contain anew_device_loginkey. compile-templates.mjsrenders every Supabase-native template with novalues, on purpose — passing real values into that step is the fastest way to overwrite Supabase's own{{ .TokenHash }}/{{ .SiteURL }}placeholders with EJS output and ship broken confirmation links.getBrandTokens(slug, registry)throws on an unrecognized brand slug in this skill'slocalfork — a deliberate correction from upstream@cogs/emails, which silently fails open to Workloom branding (brands/index.ts:55). Only--emails localgets the fail-closed fix;--emails cogsinherits the upstream gap as-is.sync-templates.mjsvalidates the full 8-key schema before any GET/PATCH and exits with zero PATCH calls on a validation failure — a malformed artifact must never cause a partial write. It never touches SMTP or captcha config fields; that's the sibling'ssync-auth-config.mjsexclusively.- The out-of-band send path requires
requireTimestamp: truealways — a fresh scaffold has no legacy v1 callers, so the replay-vulnerable body-only HMAC mode must not be offered as an option at all. - The send-service's rate limiting is in-memory and per-instance, documented as a known gap, not silently fixed — on a multi-instance or serverless deployment the effective limit scales with instance count, not one shared counter.
- A compiled artifact can sit unpushed for weeks with zero errors if
sync-templates.mjs --applyis never actually run against the live project — a real incident from the jobweave rollout (commit5c5ba1e), not a hypothetical. Verify live branding via a Management API GET onmailer_templates_*_content, not by checking that the scripts exist in the repo. - The full inventory ships, not a sample: 8 Supabase-native templates + 1
out-of-band + the shared
baselayout + all 6 partials, each with bothindex.mjmlandindex.txt. A partial inventory would silently break--emails local's all-8 default. - Zitadel/SSO federation is explicitly out of scope — adoption across the source repos was inconsistent and mostly abandoned.
Related Skills
- auth-email-provisioning — the
infra/provisioning counterpart; provisions SES/Cloudflare/Turnstile and
pushes SMTP/captcha config via its own
sync-auth-config.mjs, never touching template content. - auth-org-onboarding — closes the org-onboarding gap immediately downstream of the signup flow this skill sends confirmation/recovery email for, with no template concerns of its own.
Sourced from: skills/auth-transactional-emails/metadata.json, skills/auth-transactional-emails/SKILL.md, docs/sessions/2026-08-12-auth-email-skills/README.md, docs/sessions/2026-08-12-auth-email-skills/SUMMARY.md, docs/sessions/2026-08-12-auth-email-skills/ARCHITECTURE.md, docs/sessions/2026-08-12-auth-email-skills/FOLLOWUPS.md, docs/sessions/2026-08-12-auth-email-skills/adr/0001-skills-not-dashboard-module.md, git log --follow -- skills/auth-transactional-emails (commits d5ddcfb, 5c5ba1e)