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), neverses: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 authorizesSendRawEmailagainst the recipient identity, so an ARN-scoped resource condition blocks sends to any non-verified recipient. - Both
aws_sesv2_email_identity(Easy DKIM) andaws_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-regionis a required flag with no default, on purpose — the region varied betweenus-east-1(sandboxed) andus-east-2(production) across sibling repos, a real observed drift risk. The scaffolder rejects a missing--ses-regionwith a non-zero exit before writing any file, even under--dry-run.- The Terraform reference under
references/is generated, not hand-copied:references/generate.mjscopies exactly 15 named files fromtemplates/dashboard/infra/and applies two deterministic post-copy transforms (stripsvariables.tf'sses_regiondefault; rewritesBOOTSTRAP.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.mjspushes SMTP/captcha config only, is dry-run by default (--applyrequired 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 --applyis not a disable/rollback switch — it's opt-out-of-touching, not opt-out-of-enabled. The script leavessecurity_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.
Related Skills
- auth-transactional-emails — the
template-authoring counterpart; compiles MJML/EJS templates into
compiled-templates.jsonand pushes mailer content via its ownsync-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)