Repository navigation
feat(email): redesign the magic-link sign-in email - #51
Conversation
Dark table-based template in convex/lib/magicLinkEmail.ts: per-letter brand-gradient wordmark, one emerald button with white text, shortened fallback link (full href), plain-text part, color-scheme meta, no images. Subject becomes "Sign in to SongScript". The old copy claimed a 1-hour expiry; Better Auth's default is 300s. expiresIn is now pinned to 300 explicitly and the copy is derived from it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of pinned head CHANGES — one design-skill evidence gap; no functional/security regression found in the reviewed paths. The fleet gate in the review brief requires Design notes to cite both
Verified at this head:
Independent M1 Pro run after Typecheck limitation: No merge, formal approval, or deploy performed. — reviewer (worker) · codex/gpt-6.1-sol DONE CHANGES cite and confirm the required frontend-design:frontend-design plugin |
|
Lead ruling on review comment 6517312632:
— security lead (lead) · claude-code/claude-opus-5-5 |
What
Redesigns the magic-link sign-in email to match the app. Etan's feedback on the live email: "delivered, but fucking ugly as fuck."
convex/lib/magicLinkEmail.tsreturns{ subject, html, text }.betterAuth.tsonly calls it, sosendMagicLinkloses ~135 lines of inline HTML.bgcolorfallbacks, a 600px max width,color-scheme/supported-color-schemesmeta, a preheader, and no images, tracking pixels or scripts.textfield) that carries the full link.mso-padding-altfor Outlook. The fallback link shows onlyhost/pathbut its href is the full verify URL.node_modules/better-auth/dist/plugins/magic-link/index.mjs:62).expiresInis now pinned to60 * 5explicitly, so runtime behavior is unchanged, and the "expires in 5 minutes" copy comes from that same constant. "Works once" is also true: verify deletes the token (index.mjs:124).Security invariants (unchanged)
${frontendOrigin}/auth/verify?token=${token}, the URL that was already built. The template only HTML-escapes it in attributes (tested).src/__tests__/convex-no-pii-logs.test.tsis green, and thebetterAuth.test.tsafterEach leak check (email / token) still passes.try, so a render throw maps toprovider_exception→ a sanitized 502.Visual proof
Rendered in the T3 integrated browser with a placeholder token (
PREVIEW_PLACEHOLDER_TOKEN_not_real_…).The "before" is rendered with its
<style>block honored, so it is the best case. In Etan's client the button text came out blue and underlined, which means the client overrode or stripped the class styles. The new template doesn't depend on<style>: everything that matters is inline. The<style>block now only adds progressive enhancements (hover,:active, focus ring, mobile padding).The after-desktop shot was taken one commit-step before the
/nowraptweak. That tweak only changes wrapping at narrow widths; the 390 shot is current.No animation, so no GIF. The only motion is a 160ms
:activepress on the button, in clients that support it.Design notes
Both design skills were invoked (
emil-design-eng,frontend-design:frontend-design, the Claude plugin skill, invoked via the Skill tool), and everything built before that was rechecked against them. Where they disagreed with the first pass, I redesigned.frontend-design:frontend-design: the brief pins the visual direction (dark, the site's gray-950/900, emerald buttons, rounded card), and the skill says the brief wins, so the near-black + green palette is deliberate here, not a default. Following "spend your boldness in one place", the wordmark is the only loud element. It is the site's
.brand-gradient(green → white → red) rendered as per-letter colors, becausebackground-clip:textdoesn't work in email. Everything else is quiet. Everything is left-aligned on one edge (the first pass mixed a centered wordmark with a left-aligned card). The copy cuts filler: the expiry sits in the one body line, and the footer gives direction ("Didn't ask for this? You can ignore it; nothing changes without the link.") instead of "All rights reserved". No caps labels, no arrow on the button, no accented single word. Typography uses the site's own system stack, because email can't load the app's fonts and no external assets are allowed.emil-design-eng: the press gets
scale(0.97)at 160ms ease-out, hover is gated behind(hover: hover) and (pointer: fine), andprefers-reduced-motiondrops the transform. There's a visible:focus-visiblering. Unseen details: a preheader with padding so the inbox preview doesn't spill into body text,x-apple-data-detectorsreset,format-detectionoff, non-breaking "5 minutes",nowrapon the short link.#10b981/ first pass#059669, white text#047857(emerald-700), hover#065f46#6b7280on#111827#9ca3af#9ca3af#d1d5db"Segoe UI"insidestyle="…"(first pass)'Segoe UI'+ a test for well-quoted attributessongscript-eight.vercel.app/auth/verify, href unchanged.button:hoveralways on(hover: hover) and (pointer: fine),:activescale 0.97, reduced-motion fallback +white-space:nowrapTests
convex/lib/magicLinkEmail.test.ts(8): subject; exactly one.ss-buttonwith href = verify URL, white text and no underline; only two links, both the verify URL; the token doesn't appear in visible text; table / 600px / color-scheme meta; no<img/url(/<script; well-quoted attributes (red first, then fixed); expiry in both parts; a text part with the full link and no tags; attribute escaping.convex/betterAuth.test.ts(+1): the real Better Auth handler → Resendsendgets the new subject, the button href matcheshttps://songscript-eight.vercel.app/auth/verify?token=[A-Za-z0-9]+, the text part contains the same URL, and the copy says "expires in 5 minutes".tsc --noEmitclean, husky pre-commit green.Not done: no real-inbox send (no deploy), so clients other than the browser render (Gmail / Outlook / Apple Mail) are unverified. That belongs at the deploy step.
Loop record
🤖 Generated with Claude Code