Skip to main content

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.json must never contain a new_device_login key.
  • compile-templates.mjs renders every Supabase-native template with no values, 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's local fork — a deliberate correction from upstream @cogs/emails, which silently fails open to Workloom branding (brands/index.ts:55). Only --emails local gets the fail-closed fix; --emails cogs inherits the upstream gap as-is.
  • sync-templates.mjs validates 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's sync-auth-config.mjs exclusively.
  • The out-of-band send path requires requireTimestamp: true always — 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 --apply is never actually run against the live project — a real incident from the jobweave rollout (commit 5c5ba1e), not a hypothetical. Verify live branding via a Management API GET on mailer_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 base layout + all 6 partials, each with both index.mjml and index.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.
  • 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)