Context
DJs at WXYC share a single computer in the control room. They log out after each set and the next DJ logs in. The current username+password flow is friction-heavy: DJs forget passwords, type them wrong, or need resets mid-shift. Email OTP (one-time passcode) lets a DJ type their email, get a 6-digit code on their phone, and sign in within seconds — no password to remember, no credentials stored on the shared machine.
Changes
Add the better-auth emailOTP plugin to Backend-Service and wire up OTP email sending via SES.
1. Add emailOTP plugin to auth definition
File: shared/authentication/src/auth.definition.ts
- Import
emailOTP from better-auth/plugins and sendOTPEmail from ./email
- Add
emailOTP() to the plugins array with configuration:
disableSignUp: true — matches existing policy; only admin-created accounts can sign in
allowedAttempts: 5 — slightly more lenient than default (3) since DJs may fumble codes
otpLength: 6, expiresIn: 300 (5 minutes)
storeOTP: 'hashed' in production, 'plain' in dev/test so the built-in GET /auth/email-otp/get-verification-otp endpoint works for E2E tests
2. Add OTP email template
File: shared/authentication/src/email.ts
- New
buildOTPEmailHtml() function — shares the same WXYC-branded outer shell (logo, pink/dark theme, footer) as buildEmailHtml but replaces the action button with a large monospace OTP code display
- New
sendOTPEmail() export — maps OTP type to subject/intro text:
"sign-in" → "Your WXYC login code"
"email-verification" → "Your WXYC verification code"
"forget-password" → "Your WXYC password reset code"
- Footer: "This code expires in 5 minutes. If you didn't request this, you can safely ignore it."
- Text fallback: "Your code is: 123456. It expires in 5 minutes."
3. Tests
New file: tests/unit/authentication/email-otp.test.ts
- 9 tests across two describe blocks
buildOTPEmailHtml: OTP rendering, WXYC branding, monospace styling, custom/default footer
sendOTPEmail: parameterized test covering all 3 OTP types, plus missing SES_FROM_EMAIL error
Endpoints exposed
The emailOTP plugin automatically registers:
POST /auth/email-otp/send-verification-otp — sends an OTP to the given email
POST /auth/sign-in/email-otp — verifies OTP and creates a session
GET /auth/email-otp/get-verification-otp — returns the OTP in non-production (for E2E testing)
Deployment
No new env vars, no database migrations, no SES reconfiguration needed. The plugin uses the existing auth_verification table.
Files modified
| File |
Change |
shared/authentication/src/auth.definition.ts |
Add emailOTP plugin import + config |
shared/authentication/src/email.ts |
Add buildOTPEmailHtml, sendOTPEmail |
tests/unit/authentication/email-otp.test.ts |
New: OTP email tests |
Context
DJs at WXYC share a single computer in the control room. They log out after each set and the next DJ logs in. The current username+password flow is friction-heavy: DJs forget passwords, type them wrong, or need resets mid-shift. Email OTP (one-time passcode) lets a DJ type their email, get a 6-digit code on their phone, and sign in within seconds — no password to remember, no credentials stored on the shared machine.
Changes
Add the better-auth
emailOTPplugin to Backend-Service and wire up OTP email sending via SES.1. Add emailOTP plugin to auth definition
File:
shared/authentication/src/auth.definition.tsemailOTPfrombetter-auth/pluginsandsendOTPEmailfrom./emailemailOTP()to the plugins array with configuration:disableSignUp: true— matches existing policy; only admin-created accounts can sign inallowedAttempts: 5— slightly more lenient than default (3) since DJs may fumble codesotpLength: 6,expiresIn: 300(5 minutes)storeOTP: 'hashed'in production,'plain'in dev/test so the built-inGET /auth/email-otp/get-verification-otpendpoint works for E2E tests2. Add OTP email template
File:
shared/authentication/src/email.tsbuildOTPEmailHtml()function — shares the same WXYC-branded outer shell (logo, pink/dark theme, footer) asbuildEmailHtmlbut replaces the action button with a large monospace OTP code displaysendOTPEmail()export — maps OTP type to subject/intro text:"sign-in"→ "Your WXYC login code""email-verification"→ "Your WXYC verification code""forget-password"→ "Your WXYC password reset code"3. Tests
New file:
tests/unit/authentication/email-otp.test.tsbuildOTPEmailHtml: OTP rendering, WXYC branding, monospace styling, custom/default footersendOTPEmail: parameterized test covering all 3 OTP types, plus missing SES_FROM_EMAIL errorEndpoints exposed
The emailOTP plugin automatically registers:
POST /auth/email-otp/send-verification-otp— sends an OTP to the given emailPOST /auth/sign-in/email-otp— verifies OTP and creates a sessionGET /auth/email-otp/get-verification-otp— returns the OTP in non-production (for E2E testing)Deployment
No new env vars, no database migrations, no SES reconfiguration needed. The plugin uses the existing
auth_verificationtable.Files modified
shared/authentication/src/auth.definition.tsshared/authentication/src/email.tsbuildOTPEmailHtml,sendOTPEmailtests/unit/authentication/email-otp.test.ts