Skip to main content

auth-email-provisioning

Why It Exists

This pattern — SES + Terraform + Cloudflare DNS/Turnstile + MJML templates synced into Supabase's mailer, plus an out-of-band HMAC-signed send-service — was proven across roughly 13 workloom-family repos but existed only as duplicated, drifting code, not a reusable, documented, scaffoldable pattern. docs/HARVEST-AUTH-INFRA.md, a pre-existing survey of that same pattern, recommended a different shape: bake it into templates/dashboard's existing opt-in auth module and consume @cogs/auth, @cogs/auth-events, @cogs/send-service, and @cogs/supabase-sync directly.

adr/0001-skills-not-dashboard-module.md (from the docs/sessions/2026-08-12-auth-email-skills/ dossier) rejected that recommendation. Two of the harvest plan's four target @cogs/* 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 a module that silently didn't work for anyone outside the @cogs family. The ADR chose two standalone skills instead, split by lifecycle (provisioning vs. template authoring) and bridged by one versioned artifact rather than a shared script, specifically so either half could be scaffolded into a template-less app or a future template — not locked to templates/dashboard. The cost, tracked in that session's FOLLOWUPS.md, is that the dashboard template's own auth module is now a second, not-yet-reconciled implementation of a closely related pattern: its infra/BOOTSTRAP.md still references @cogs/* directly and hasn't been switched over to consume this skill's scripts.

The invariants inside this skill also trace back to concrete defects caught during that session's four rounds of codex critic review, not hypothetical caution. The write-only-secret rotation logic in sync-auth-config.mjs originally had an idempotency rule that would have silently eaten legitimate secret rotations; it was rewritten so --rotate-smtp-password/--rotate-captcha-secret are explicit escape hatches independent of whether the anchor fields (host/user, captcha enabled/provider) changed. The generated BOOTSTRAP.md's @cogs/* references were also caught as stale during review and replaced with pointers to this skill's own scripts and the sibling send-service example.

What It Does

auth-email-provisioning is the infra/provisioning half of the auth-email pattern. Terraform (generated and drift-tested from templates/dashboard/infra/) stands up an SES identity with Easy DKIM, a dedicated MAIL FROM bounce subdomain, IAM scoped to ses:FromAddress, Cloudflare-managed DNS records for DKIM/SPF/DMARC, and a Turnstile widget, all behind a shared S3+KMS+DynamoDB remote Terraform backend. Once applied, this skill's sync-auth-config.mjs reads terraform output -json and pushes the resulting SMTP credentials and Turnstile captcha secret into Supabase Auth's config — SMTP/captcha only, never template content.

The sibling skill auth-transactional-emails owns the other half — MJML/EJS template authoring and its own sync-templates.mjs push of mailer template content. Neither skill's scripts cross into the other's scope.

How To Use It

Triggers on: "provision SES", "SES + Terraform + Cloudflare", "DKIM/SPF/DMARC for auth email", "ses:FromAddress vs ses:FromDomain", "Turnstile widget terraform", "--ses-region required", "sync-auth-config.mjs", "push Terraform SMTP creds into Supabase", "Vercel wildcard CNAME hazard", or standing up the SES/Cloudflare/Turnstile infra half of branded auth email.

skills add git@github.com:catesandrew/next-starters.git --skill skills/auth-email-provisioning -g
npm install @next-starters/skill-auth-email-provisioning
/plugin marketplace add catesandrew/next-starters
/plugin install auth-email-provisioning@next-starters

Gotchas & Invariants

  • IAM must be scoped by ses:FromAddress (StringLike), never ses:FromDomain — that key doesn't exist in SES at all, so a policy written against it matches nothing and silently denies every send. Don't scope to the identity ARN either: SES sandbox also authorizes SendRawEmail against the recipient identity, so an ARN-scoped resource condition blocks sends to any non-verified recipient.
  • Both aws_sesv2_email_identity (Easy DKIM) and aws_sesv2_email_identity_mail_from_attributes (bounce subdomain) are required — DKIM alone still fails SPF alignment on the envelope sender.
  • The Vercel wildcard hazard: Vercel's own *.<domain> wildcard is never a Cloudflare record Terraform manages, so any explicitly-provisioned host (e.g. api.<domain>) gets silently swallowed by it and 404s unless a defensive CNAME explicitly wins over the wildcard.
  • --ses-region is a required flag with no default, on purpose — the region varied between us-east-1 (sandboxed) and us-east-2 (production) across sibling repos, a real observed drift risk. The scaffolder rejects a missing --ses-region with a non-zero exit before writing any file, even under --dry-run.
  • The Terraform reference under references/ is generated, not hand-copied: references/generate.mjs copies exactly 15 named files from templates/dashboard/infra/ and applies two deterministic post-copy transforms (strips variables.tf's ses_region default; rewrites BOOTSTRAP.md's two distinct @cogs/* instruction blocks). The CI drift test diffs only those 15 named files, never a whole-directory diff.
  • sync-auth-config.mjs pushes SMTP/captcha config only, is dry-run by default (--apply required to write), never accepts or prints a secret value, and issues two ordered PATCH groups — SMTP/config first, rate-limits second — because the Management API can reject a rate-limit change bundled with first-time SMTP setup.
  • --with-captcha false --apply is not a disable/rollback switch — it's opt-out-of-touching, not opt-out-of-enabled. The script leaves security_captcha_* fields alone when captcha is already off, reporting "Already reconciled" even while captcha is still live. This false negative delayed a real rollback in a live incident; rolling back requires bypassing the script with a direct Management API PATCH.
  • references/hardened/*.tf (prevent_destroy, apex DMARC/null-MX, an IAM naming guard) are whole-file swaps hand-ported from studio-os, not part of the drift-tested 15-file manifest, with no automated drift test against studio-os — an accepted, documented manual-sync risk.
  • Zitadel/SSO federation is explicitly out of scope: adoption across the source repos was inconsistent and mostly abandoned.
  • auth-transactional-emails — the template-authoring counterpart; compiles MJML/EJS templates into compiled-templates.json and pushes mailer content via its own sync-templates.mjs, never touching SMTP/captcha config.
  • auth-org-onboarding — closes the org-onboarding gap immediately downstream of this signup flow, when a new signup with zero org membership hits a 403 on every org-gated page.

Sourced from: skills/auth-email-provisioning/metadata.json, skills/auth-email-provisioning/SKILL.md, docs/sessions/2026-08-12-auth-email-skills/ARCHITECTURE.md, docs/sessions/2026-08-12-auth-email-skills/SUMMARY.md, docs/sessions/2026-08-12-auth-email-skills/README.md, docs/sessions/2026-08-12-auth-email-skills/adr/0001-skills-not-dashboard-module.md, docs/sessions/2026-08-12-auth-email-skills/FOLLOWUPS.md, git log --follow -- skills/auth-email-provisioning (commit d5ddcfb)