Email 6-Digit Code Sign-In (OTP)
A passwordless sign-in path that emails the user a 6-digit code to type instead of a magic link to click. This is the reliable option for corporate inboxes.
Why this exists
Enterprise mail security (Proofpoint, Mimecast, Microsoft Defender Safe Links,
Barracuda, etc.) rewrites and pre-fetches URLs in inbound email. When it
opens a Supabase magic link to βscanβ it, GoTrue consumes the one-time token β
so by the time the user clicks, the link is already spent and they see
otp_expired / access_denied. There is nothing we can do server-side to stop
a scanner from following a link.
A typed 6-digit code sidesteps the problem entirely: it is inert text the
scanner cannot βclick,β and verification is a separate GoTrue path from the
PKCE ?code= link exchange. The user reads the number and types it into the
form.
We keep the magic link as the default for everyone else; the code is an opt-in fallback offered on the login screen (βEmail me a 6-digit code insteadβ) and auto-recommended in the error hint after a burned-link failure.
How it works
- The same
signInWithOtp({ email })call powers both paths β Supabaseβs OTP email carries a magic link and a{{ .Token }}6-digit code. - The code-entry UI calls
supabase.auth.verifyOtp({ email, token, type: 'email' }). This works regardless of the clientβs PKCE flow type (PKCE only governs the link/?code=exchange). - Default code lifetime is 1 hour and single-use (Supabase Auth β OTP expiry; can be lowered to e.g. 600s).
Required Supabase Dashboard step (one-time)
The code path only works if the Magic Link email template actually renders
the token. Add {{ .Token }} to the template body. Keep the link too so
existing magic-link users are unaffected.
-
Supabase Dashboard β Authentication β Email Templates β Magic Link.
-
Replace the body with the template below. It leads with the code (the reliable path) and keeps the
{{ .ConfirmationURL }}link as a styled fallback button. Notes on the design:- Canonical product name
TheAccessiblePDFβ never the retired βAccessible PDF Converterβ (project naming rule). - One expiry value. Supabase email-OTP default is 1 hour β donβt reintroduce a β24 hoursβ line; it contradicts the real token lifetime.
- Tells the user where to enter the code (back on the sign-in page).
- Blue accent
#054fb9(AAA on white); fallback is a 44px-tall button.
<div style="background-color:#f8fafc;padding:40px 20px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;"><div style="max-width:600px;margin:0 auto;background-color:#ffffff;border-radius:8px;overflow:hidden;"><!-- Header --><div style="padding:32px 40px 24px 40px;border-bottom:1px solid #e2e8f0;"><span style="font-size:22px;font-weight:700;color:#1f2937;">TheAccessiblePDF</span></div><!-- Body --><div style="padding:40px;"><h2 style="margin:0 0 16px 0;font-size:24px;font-weight:600;color:#1f2937;">Your sign-in code</h2><p style="margin:0 0 24px 0;font-size:16px;line-height:1.6;color:#1f2937;">We received a request to sign in to TheAccessiblePDF. Go back to thesign-in page in your browser β the tab where you asked for a code βand type this code into the <strong>“6-digit code”</strong> box:</p><!-- The code (hero) --><div style="margin:0 0 8px 0;padding:20px;background-color:#eff6ff;border:1px solid #b8d0f8;border-radius:8px;text-align:center;"><span style="font-size:34px;font-weight:700;letter-spacing:8px;color:#054fb9;font-family:'SF Mono',SFMono-Regular,Menlo,Consolas,monospace;">{{ .Token }}</span></div><p style="margin:0 0 32px 0;font-size:14px;line-height:1.5;color:#6b7280;">This code expires in 1 hour and can be used once. If you closed thetab, return to the sign-in page, choose “Email me a 6-digitcode,” enter your email, and type this code.</p><!-- Fallback link (secondary) --><p style="margin:0 0 12px 0;font-size:14px;line-height:1.5;color:#6b7280;">Prefer a link? Some corporate email systems break sign-in links, sothe code above is more reliable β but you can also:</p><table role="presentation" cellpadding="0" cellspacing="0" style="margin:0 0 8px 0;"><tr><td style="border-radius:8px;background-color:#054fb9;"><a href="{{ .ConfirmationURL }}" style="display:inline-block;min-width:200px;padding:14px 28px;font-size:16px;font-weight:600;color:#ffffff;text-decoration:none;text-align:center;">Sign in with a link</a></td></tr></table><!-- Security note --><p style="margin:24px 0 0 0;font-size:14px;line-height:1.5;color:#6b7280;border-top:1px solid #e2e8f0;padding-top:24px;">If you didn’t request this email, you can safely ignore it. Nochanges will be made to your account.</p></div><!-- Footer --><div style="padding:24px 40px;background-color:#f8fafc;border-top:1px solid #e2e8f0;text-align:center;"><p style="margin:0;font-size:13px;color:#6b7280;"><a href="https://pdf.theaccessible.org/privacy" style="color:#6b7280;text-decoration:underline;">Privacy Policy</a> · <a href="https://pdf.theaccessible.org/support" style="color:#6b7280;text-decoration:underline;">Support</a></p></div></div></div> - Canonical product name
-
Save. No redeploy of the apps is required for the template change itself. Also set a matching plain-text version in the templateβs text field (global email standard) β e.g.
Your TheAccessiblePDF sign-in code is {{ .Token }}. Enter it on the sign-in page (the tab where you requested it). Expires in 1 hour.
Verified safe: the Magic Link template is consumed only by the email
signInWithOtplogin path. Password reset (βReset Passwordβ template), signup confirmation (βConfirm signupβ), OAuth, and voice OTP (Send SMS Hook) use other templates/flows. The two server-sidegenerateLink({ type: 'magiclink' })callers β LTI (workers/api/src/routes/lti.ts) and the promo sign-in (workers/api/src/routes/promo.ts) β never make Supabase send email; they extract the token/action_link and either verify it in-process or dispatch their own email via Resend, so they donβt render this template either. The/auth/callbackhandler reads the?code=query param, not the template body, so the link keeps working.
Sibling template: Reset Password
Do NOT paste the 6-digit {{ .Token }} code into the Reset Password
template. The appβs recovery flow is link-only: the recovery link lands on
/auth/callback?type=recovery, which forwards to /auth/update-password and
calls updateUser({ password }). There is no recovery-code entry screen, so a
code in this email points nowhere. (Building verifyOtp({ type: 'recovery' })
- a
/auth/reset-codepage is a possible future task; only then would a code belong here.)
Because the recovery link has the same corporate-scanner risk as the magic link, the reset email should give those users an escape hatch: skip the password and sign in passwordlessly with a code. Corrected template:
<div style="background-color:#f8fafc;padding:40px 20px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;"> <div style="max-width:600px;margin:0 auto;background-color:#ffffff;border-radius:8px;overflow:hidden;"> <!-- Header --> <div style="padding:32px 40px 24px 40px;border-bottom:1px solid #e2e8f0;"> <span style="font-size:22px;font-weight:700;color:#1f2937;">TheAccessiblePDF</span> </div>
<!-- Body --> <div style="padding:40px;"> <h2 style="margin:0 0 16px 0;font-size:24px;font-weight:600;color:#1f2937;">Reset your password</h2> <p style="margin:0 0 28px 0;font-size:16px;line-height:1.6;color:#1f2937;"> We received a request to reset the password for your TheAccessiblePDF account. Click the button below to choose a new password. This link expires in 1 hour and can be used once. </p>
<!-- Primary CTA --> <table role="presentation" cellpadding="0" cellspacing="0" style="margin:0 0 24px 0;"> <tr> <td style="border-radius:8px;background-color:#054fb9;"> <a href="{{ .ConfirmationURL }}" style="display:inline-block;min-width:200px;padding:14px 28px;font-size:16px;font-weight:600;color:#ffffff;text-decoration:none;text-align:center;"> Reset your password </a> </td> </tr> </table>
<!-- Corporate-filter escape hatch --> <p style="margin:0 0 24px 0;font-size:14px;line-height:1.5;color:#6b7280;"> Using a work email and the button doesn’t work? Your company’s security scanner may have opened the link first. You can skip the password entirely β go to the <a href="https://pdf.theaccessible.org/auth/login" style="color:#054fb9;text-decoration:underline;">sign-in page</a> and choose “Email me a 6-digit code.” </p>
<!-- Security note --> <p style="margin:0;font-size:14px;line-height:1.5;color:#6b7280;border-top:1px solid #e2e8f0;padding-top:24px;"> If you didn’t request a password reset, you can safely ignore this email. Your password will remain unchanged. </p> </div>
<!-- Footer (TheAccessible.org branded β NOT AnglinAI) --> <div style="padding:24px 40px;background-color:#f8fafc;border-top:1px solid #e2e8f0;text-align:center;"> <p style="margin:0 0 8px 0;font-size:13px;color:#6b7280;"> <a href="https://pdf.theaccessible.org/privacy" style="color:#6b7280;text-decoration:underline;">Privacy Policy</a> · <a href="https://pdf.theaccessible.org/support" style="color:#6b7280;text-decoration:underline;">Support</a> </p> <p style="margin:0;font-size:13px;color:#6b7280;">© 2026 TheAccessible.org. All rights reserved.</p> </div> </div></div>Keep the expiry number (1 hour) in sync with the Auth Email OTP Expiration setting. Set a matching plain-text version too.
Sibling template: Confirm Signup
Link-based confirmation (Supabase {{ .ConfirmationURL }}). Keep it link-based β
there is no signup-code entry screen β but fix the name, off-brand button color,
and AnglinAI footer, and add the passwordless escape hatch. (Signing in with a
6-digit code also verifies the email, so itβs a genuine alternative when a
corporate scanner burns the confirm link.) Corrected template:
<div style="background-color:#f8fafc;padding:40px 20px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;"> <div style="max-width:600px;margin:0 auto;background-color:#ffffff;border-radius:8px;overflow:hidden;"> <!-- Header --> <div style="padding:32px 40px 24px 40px;border-bottom:1px solid #e2e8f0;"> <span style="font-size:22px;font-weight:700;color:#1f2937;">TheAccessiblePDF</span> </div>
<!-- Body --> <div style="padding:40px;"> <h2 style="margin:0 0 16px 0;font-size:24px;font-weight:600;color:#1f2937;">Confirm your email address</h2> <p style="margin:0 0 28px 0;font-size:16px;line-height:1.6;color:#1f2937;"> Thanks for signing up for TheAccessiblePDF. Click the button below to verify your email address and activate your account. </p>
<!-- Primary CTA --> <table role="presentation" cellpadding="0" cellspacing="0" style="margin:0 0 24px 0;"> <tr> <td style="border-radius:8px;background-color:#054fb9;"> <a href="{{ .ConfirmationURL }}" style="display:inline-block;min-width:200px;padding:14px 28px;font-size:16px;font-weight:600;color:#ffffff;text-decoration:none;text-align:center;"> Confirm your email </a> </td> </tr> </table>
<!-- Copy-paste fallback --> <p style="margin:0 0 24px 0;font-size:14px;line-height:1.5;color:#6b7280;"> If the button doesn’t work, copy and paste this link into your browser:<br /> <a href="{{ .ConfirmationURL }}" style="color:#054fb9;word-break:break-all;">{{ .ConfirmationURL }}</a> </p>
<!-- Corporate-filter escape hatch --> <p style="margin:0 0 24px 0;font-size:14px;line-height:1.5;color:#6b7280;"> Using a work email and neither the button nor the link works? Your company’s security scanner may have opened the link first. You can also just sign in β go to the <a href="https://pdf.theaccessible.org/auth/login" style="color:#054fb9;text-decoration:underline;">sign-in page</a> and choose “Email me a 6-digit code.” Signing in verifies your email too. </p>
<!-- Security note --> <p style="margin:0;font-size:14px;line-height:1.5;color:#6b7280;border-top:1px solid #e2e8f0;padding-top:24px;"> If you didn’t create an account, you can safely ignore this email. </p> </div>
<!-- Footer (TheAccessible.org branded β NOT AnglinAI) --> <div style="padding:24px 40px;background-color:#f8fafc;border-top:1px solid #e2e8f0;text-align:center;"> <p style="margin:0 0 8px 0;font-size:13px;color:#6b7280;"> <a href="https://pdf.theaccessible.org/privacy" style="color:#6b7280;text-decoration:underline;">Privacy Policy</a> · <a href="https://pdf.theaccessible.org/support" style="color:#6b7280;text-decoration:underline;">Support</a> </p> <p style="margin:0;font-size:13px;color:#6b7280;">© 2026 TheAccessible.org. All rights reserved.</p> </div> </div></div>Plain-text versions
Global email standard requires a plain-text alternative for every email. Note:
Supabaseβs dashboard email templates are HTML-only β there is no plain-text
field in the UI, and GoTrueβs built-in mailer sends a single HTML part. To ship
a real multipart/alternative text part you must move sending to the Send
Email Hook (custom SMTP / Resend), where you emit both parts yourself; use the
bodies below as the text part there. Until then, keep these in sync with the
HTML so theyβre ready to drop in.
Placeholders ({{ .Token }}, {{ .ConfirmationURL }}) resolve the same way in
the hook payload. Keep every line under ~78 chars and avoid trailing spaces.
Magic Link (sign-in code)
TheAccessiblePDF β your sign-in code
We received a request to sign in to TheAccessiblePDF.
Your 6-digit code is: {{ .Token }}
Go back to the sign-in page in your browser β the tab where you asked for acode β and type it into the "6-digit code" box. This code expires in 1 hourand can be used once.
If you closed that tab, return to the sign-in page, choose "Email me a6-digit code," enter your email, and type this code.
Prefer a link? Some corporate email systems break sign-in links, so the codeabove is more reliable. Link: {{ .ConfirmationURL }}
If you didn't request this email, you can safely ignore it. No changes willbe made to your account.
Privacy: https://pdf.theaccessible.org/privacySupport: https://pdf.theaccessible.org/support(c) 2026 TheAccessible.org. All rights reserved.Reset Password
TheAccessiblePDF β reset your password
We received a request to reset the password for your TheAccessiblePDFaccount. Open this link to choose a new password (expires in 1 hour, singleuse):
{{ .ConfirmationURL }}
Using a work email and the link doesn't work? Your company's securityscanner may have opened it first. You can skip the password entirely β go tohttps://pdf.theaccessible.org/auth/login and choose "Email me a 6-digitcode."
If you didn't request a password reset, you can safely ignore this email.Your password will remain unchanged.
Privacy: https://pdf.theaccessible.org/privacySupport: https://pdf.theaccessible.org/support(c) 2026 TheAccessible.org. All rights reserved.Confirm Signup
TheAccessiblePDF β confirm your email address
Thanks for signing up for TheAccessiblePDF. Open this link to verify youremail address and activate your account:
{{ .ConfirmationURL }}
Using a work email and the link doesn't work? Your company's securityscanner may have opened it first. You can also just sign in β go tohttps://pdf.theaccessible.org/auth/login and choose "Email me a 6-digitcode." Signing in verifies your email too.
If you didn't create an account, you can safely ignore this email.
Privacy: https://pdf.theaccessible.org/privacySupport: https://pdf.theaccessible.org/support(c) 2026 TheAccessible.org. All rights reserved.Where itβs wired
packages/auth/src/create-auth-provider.tsxβverifyEmailOtp(email, code)(verifyOtpwithtype: 'email'), exposed on the auth context.packages/accessible-org-ui/src/components/EmailOtpLogin.tsxβ two-step (email β code) UI, mirrorsVoiceOtpLogin.packages/accessible-org-ui/src/components/LoginPage.tsxβemailOtpprop + βEmail me a 6-digit code insteadβ toggle + burned-link error hint.apps/web/src/app/auth/login/page.tsxβ passesemailOtp={{ onRequestCode: signInWithEmail, onVerify: verifyEmailOtp }}.
Enabling it in other apps
Every app uses the shared SharedAuthPage/LoginPage and the base auth
provider, so verifyEmailOtp is already available on their useAuth(). To
turn the feature on for another app, add one prop to that appβs login page:
emailOtp={{ onRequestCode: signInWithEmail, onVerify: verifyEmailOtp }}