Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b02bb55
docs(plans): add email confirmation signup plan for #164
emsqrd May 9, 2026
4c2ddd5
docs(plans): split #164 plan into six commits; correct Mailpit API usage
emsqrd May 11, 2026
8a4a214
docs: rename Inbucket label to Mailpit; link CLAUDE.md topology pointer
emsqrd May 11, 2026
23ca5ec
feat(web): add PKCE flow and /auth/callback loader
emsqrd May 11, 2026
a02cf2d
docs(plans): reflect loader-based auth callback design
emsqrd May 11, 2026
7cc9600
docs(plans): swap commit 2 to hand-rolled OtpInput primitive
emsqrd May 12, 2026
d4a632b
feat(web): add OtpInput primitive + CheckEmailNotice pending UI
emsqrd May 13, 2026
4bab4df
docs(plans): sync #164 plan with what commit 2 actually shipped
emsqrd May 13, 2026
ab7b471
docs(plans): reorder #164 commits and add integration tests
emsqrd May 13, 2026
de592bc
feat(web): enable email confirmation on signup
emsqrd May 13, 2026
69351be
docs(plans): sync #164 plan with what commit 3 actually shipped
emsqrd May 13, 2026
4922307
docs(plans): rework #164 to implicit-flow design
emsqrd May 13, 2026
93fa04a
refactor(web): rework email confirmation to implicit-flow defaults
emsqrd May 13, 2026
55bee8b
docs(plans): drop status block and commit-heading markers for #164
emsqrd May 13, 2026
abf4ed9
feat(web): route authed users from / and thread sign-up redirect
emsqrd May 14, 2026
456dd6d
docs(plans): sync #164 plan with what commit 5 actually shipped
emsqrd May 14, 2026
0f3caaa
docs: add commenting strategy to CLAUDE.md
emsqrd May 14, 2026
6e7da5f
feat(web): route failed confirmation links to /sign-up with inline error
emsqrd May 14, 2026
efc9fbd
feat(web): add resend confirmation email
emsqrd May 14, 2026
9ed9f2a
docs(plans): sync #164 plan with what commit 7 actually shipped
emsqrd May 14, 2026
bab8386
test(e2e): cover email-confirmation signup paths
emsqrd May 15, 2026
9bfb5dd
docs(plans): sync #164 plan with what commit 8 actually shipped
emsqrd May 15, 2026
fe0caa1
test(e2e): cover resend confirmation email path
emsqrd May 15, 2026
b4dfab2
docs(plans): sync #164 plan with what commit 9 actually shipped
emsqrd May 15, 2026
ca1bf3e
fix(auth): clear stuck overlay and capture unexpected verifyOtp errors
emsqrd May 15, 2026
d127a2b
Merge branch 'main' into email-confirmation-signup-issue-164
emsqrd May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,37 @@ Overlap is correct when each layer catches a distinct failure mode. It's waste w
- Frontend unit/component tests: Vitest + `@testing-library/react`.
- E2E: Playwright, small suite, runs against a prod-like build. Parallel workers with isolated users.

## Commenting Strategy

Default to no comment. Every comment is debt — it can rot, mislead, or distract. Write one only when the alternative is worse: a reader would otherwise misunderstand the code or spend time discovering something the comment encodes directly.

### The test before writing

Ask: **"Would removing this confuse a competent reader six months from now, reading the code cold?"** If no — cut it. Trust the names, the types, and the structure to carry the meaning.

### When a comment earns its place

- **Non-obvious WHY.** A constraint or invariant that wouldn't show up in the AST. _"Caller must hold the write lock." "API returns 200 with an error body for this case — peek at the JSON."_
- **A surprise.** Behavior a reader would assume is a bug. _"Off-by-one is intentional — F1 rounds are 1-indexed." "We retry on 401 — auth service emits them during clock skew."_
- **Workaround pointer.** External constraint forcing a non-obvious shape. _"Chrome bug crbug.com/123456 — drop when our floor is M120."_
- **Domain pointer.** Spec/RFC/paper that explains the rule. _"RFC 7231 §6.4.3: 303 always GETs the target."_

### When a comment doesn't

- **What the code or types already say.** `// Iterate users` above a `for` loop; `// Returns 'expired' or null` above `(): Promise<'expired' | null>`. Rename, don't comment.
- **Task context.** _"Fix for #164." "Per the design doc."_ Belongs in the commit message; rots when issues close.
- **Historical state.** _"Was synchronous before."_ `git log` owns this; the file describes what is, not what was.
- **Internal callers / provenance.** _"Used by SignUpForm." "Set by indexRoute.beforeLoad."_ Find-references gives you this for free; comment goes stale on the next caller.
- **Design rationale.** _"We chose route context over AuthContext because…"_ Lives in `docs/plans/`, not the source file. The code is the chosen path; the argument is dead weight once the choice is made.
- **Reassurance about performance/correctness of an implementation detail.** _"This is cached, so it's free to call."_ Reader doesn't need it.

### Heuristics

- Tempted to write _"// this X-es the Y"_ → rename instead.
- Tempted to write _"// because Z"_ → is Z visible in the code (keep) or only in your head / the conversation / the plan (cut).
- Watch the diff-talk smell: _"now uses," "still works," "originally was"_ narrate history, not state.
- Read it cold. If a reader needs to know "the resend flow" or "the X migration" to parse the comment, rewrite in plain terms or delete.

## Git Commit Message Preferences

- Do not include the "Generated with Claude Code" footer in commit messages or PR descriptions
Expand Down Expand Up @@ -212,7 +243,7 @@ Open this folder in VSCode and use:

## Local Services Topology

See `README.md` → "Local Services Topology" for the dev vs e2e stack layout, port-shift rule, and migration-sharing details. Quick recall: dev processes use Supabase `54321–54324` / web `5173` / API `5077`; e2e processes are all shifted by +100; `api/supabase/migrations/` is the source of truth and e2e symlinks to it.
See [Local Services Topology](README.md#local-services-topology) in the README for the dev vs e2e stack layout, port-shift rule, and migration-sharing details. Quick recall: dev processes use Supabase `54321–54324` / web `5173` / API `5077`; e2e processes are all shifted by +100; `api/supabase/migrations/` is the source of truth and e2e symlinks to it.

## Production Infrastructure

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ npm run test:all # Frontend + backend (unit + integration). Does n

Two Supabase CLI stacks and two sets of web/API servers coexist on one machine so dev work and the e2e suite never touch the same state.

| Stack | Config | Supabase ports (API/DB/Studio/Inbucket) | Web port | API port |
| Stack | Config | Supabase ports (API/DB/Studio/Mailpit) | Web port | API port |
| ----- | --------------- | --------------------------------------- | -------- | -------- |
| Dev | `api/supabase/` | `54321 / 54322 / 54323 / 54324` | `5173` | `5077` |
| E2E | `e2e/supabase/` | `54421 / 54422 / 54423 / 54424` | `5273` | `5177` |
Expand Down
13 changes: 9 additions & 4 deletions api/supabase/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,10 @@ file_size_limit = "50MiB"
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
site_url = "http://localhost:5173"
# A list of URLs that auth providers are permitted to redirect to post
# authentication. Wildcard supported.
additional_redirect_urls = ["http://localhost:5173/**"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 604800
# Path to JWT signing key. DO NOT commit your signing keys file to git.
Expand Down Expand Up @@ -173,7 +174,7 @@ enable_signup = true
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
enable_confirmations = true
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
Expand All @@ -198,6 +199,10 @@ otp_expiry = 3600
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"

[auth.email.template.confirmation]
subject = "Confirm your F1 Fantasy email"
content_path = "./supabase/templates/confirmation.html"

[auth.sms]
# Allow/disallow new user signups via SMS to your project.
enable_signup = false
Expand Down
255 changes: 255 additions & 0 deletions api/supabase/templates/confirmation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<meta name="x-apple-disable-message-reformatting" />
<title>Confirm your email — F1 Fantasy</title>
<style>
/* Reset (clients ignore most, but harmless) */
body,
table,
td,
p,
a {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border-collapse: collapse;
}
img {
-ms-interpolation-mode: bicubic;
border: 0;
outline: none;
text-decoration: none;
}

/* Dark mode for clients that honour prefers-color-scheme (Apple Mail, iOS, etc.) */
@media (prefers-color-scheme: dark) {
body,
.bg {
background: #18181b !important;
}
.text-strong {
color: #fafafa !important;
}
.text-dim {
color: #a1a1aa !important;
}
.text-faint {
color: #71717b !important;
}
.rule {
border-color: rgba(255, 255, 255, 0.1) !important;
}
.btn {
background: #fafafa !important;
color: #18181b !important;
}
.brand-stroke {
stroke: #fafafa !important;
}
.link {
color: #71717b !important;
}
}

/* Mobile */
@media only screen and (max-width: 520px) {
.container {
width: 100% !important;
padding-left: 24px !important;
padding-right: 24px !important;
}
.h1 {
font-size: 24px !important;
line-height: 1.22 !important;
}
.cta-row {
display: block !important;
}
.cta-row .cta-aside {
display: block !important;
margin: 10px 0 0 0 !important;
}
}

/* Force solid background on the page in Outlook desktop */
body {
margin: 0 !important;
padding: 0 !important;
}
</style>
</head>
<body class="bg" style="margin: 0; padding: 0; background: #ffffff">
<!-- Hidden preheader -->
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all; font-size: 1px; line-height: 1px; color: #ffffff">
Confirm your email to start drafting your F1 Fantasy team. Code: {{ .Token }}
</div>

<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background: #ffffff">
<tr>
<td align="center" style="padding: 56px 16px 48px">
<table
role="presentation"
class="container"
width="480"
cellpadding="0"
cellspacing="0"
border="0"
style="max-width: 480px; width: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
>
<!-- Wordmark -->
<tr>
<td style="padding: 0 0 40px">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td valign="middle" style="padding-right: 8px; line-height: 0">
<!-- Trophy mark -->
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#09090b"
class="brand-stroke"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6" />
<path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18" />
<path d="M4 22h16" />
<path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22" />
<path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22" />
<path d="M18 2H6v7a6 6 0 0 0 12 0V2Z" />
</svg>
</td>
<td valign="middle" class="text-strong" style="font-size: 13px; font-weight: 600; color: #09090b; letter-spacing: 0.04em">
F1 Fantasy
</td>
</tr>
</table>
</td>
</tr>

<!-- Headline -->
<tr>
<td style="padding: 0 0 14px">
<h1
class="h1 text-strong"
style="margin: 0; font-size: 28px; font-weight: 600; color: #09090b; letter-spacing: -0.022em; line-height: 1.2"
>
You're on the grid.
</h1>
</td>
</tr>

<!-- Body paragraph -->
<tr>
<td class="text-dim" style="padding: 0 0 28px; font-size: 15.5px; color: #52525b; line-height: 1.65">
Confirm your email below and you can start drafting drivers, building your team, and joining leagues. The link expires in
60&nbsp;minutes.
</td>
</tr>

<!-- CTA + aside -->
<tr>
<td style="padding: 0 0 36px">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" class="cta-row">
<tr>
<td valign="middle">
<!-- Bulletproof button -->
<a
href="{{ .ConfirmationURL }}"
class="btn"
style="
display: inline-block;
padding: 11px 20px;
background: #09090b;
color: #ffffff;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
letter-spacing: -0.005em;
font-family: inherit;
"
>
Confirm your email
</a>
</td>
<td valign="middle" class="cta-aside text-faint" style="padding-left: 12px; font-size: 13px; color: #71717b">
or paste the code below
</td>
</tr>
</table>
</td>
</tr>

<!-- Hairline + code row -->
<tr>
<td class="rule" style="border-top: 1px solid #e4e4e7; font-size: 0; line-height: 0">&nbsp;</td>
</tr>
<tr>
<td class="rule" style="padding: 16px 0 18px; border-bottom: 1px solid #e4e4e7">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td valign="middle" class="text-faint" style="font-size: 12px; color: #71717b; letter-spacing: 0.04em">Verification code</td>
<td
valign="middle"
align="right"
class="text-strong"
style="
font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
font-size: 22px;
font-weight: 600;
color: #09090b;
letter-spacing: 0.22em;
"
>
{{ .Token }}
</td>
</tr>
</table>
</td>
</tr>

<!-- Sign-off -->
<tr>
<td style="padding: 32px 0 0">
<p class="text-dim" style="margin: 0 0 6px; font-size: 14.5px; color: #52525b; line-height: 1.6">Welcome aboard,</p>
<p class="text-strong" style="margin: 0; font-size: 14.5px; color: #09090b; font-weight: 500">The F1 Fantasy team</p>
</td>
</tr>

<!-- Postscript -->
<tr>
<td class="text-faint" style="padding: 32px 0 0; font-size: 12.5px; color: #71717b; line-height: 1.6">
<span class="text-dim" style="color: #52525b; font-weight: 500">P.S.</span>
Didn't sign up for F1 Fantasy? You can safely ignore this email — no account will be created.
</td>
</tr>

<!-- Outer footer -->
<tr>
<td style="padding: 40px 0 0; font-size: 11.5px; color: #71717b; line-height: 1.6" class="text-faint">
<a href="#" class="link" style="color: #71717b; text-decoration: underline">Unsubscribe</a>
<span> · </span>
<a href="#" class="link" style="color: #71717b; text-decoration: underline">Help</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
Loading
Loading