feat: Implement user auth with Cal Poly email verification#21
Conversation
- Add user authentication using @convex-dev/auth with Password provider - Enforce @calpoly.edu email domain validation - Implement email verification flow with SendGrid integration - Create signup, login, and email verification screens - Add user schema and auth-related queries/mutations - Update shared types and utilities for email validation - Add AuthProvider to app layout with auth routes
evan-taylor
left a comment
There was a problem hiding this comment.
Overall, I think the implementation is super solid! A few configuration things I added!
- Replace Password provider with Resend OTP (8-digit code, 15 min expiry) - Add Cal Poly email domain validation - Simplify to single login screen (email → code) - Remove separate signup and verify-email screens - Switch from SendGrid to Resend for emails - Use @convex-dev/auth OTP pattern per reviewer feedback
📝 WalkthroughWalkthroughAdds email OTP authentication via a Resend-based provider and Convex auth integration: new backend auth/http modules, users schema and APIs, ResendOTP provider, frontend login screen and auth hook, shared type/util updates, package.json and env/docs adjustments. Changes
Sequence DiagramsequenceDiagram
participant User
participant UI as Login Screen
participant Convex as Convex Backend
participant Resend as Resend API
participant DB as Convex DB
User->>UI: Submit Cal Poly email
UI->>Convex: signIn(email)
Convex->>Convex: validate email & generate OTP
Convex->>Resend: sendEmail(OTP)
Resend-->>User: deliver OTP email
User->>UI: Enter OTP
UI->>Convex: verify OTP
Convex->>DB: get/create user profile
DB-->>Convex: user record
Convex-->>UI: auth success / session
UI->>Convex: getCurrentUser
Convex->>DB: fetch user by email
DB-->>Convex: user data
Convex-->>UI: return profile
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@backend/convex/users.ts`:
- Around line 16-26: The code uses a non-null assertion on authUser.email when
querying users (authUser and the query that builds userProfile via
ctx.db.query('users').withIndex('by_email', ...).first()); add a guard to check
authUser.email exists before running the query (e.g., if (!authUser.email)
return null or handle appropriately) so the withIndex call never receives a
null/undefined email and avoid runtime errors.
In `@backend/package.json`:
- Around line 13-16: The docs still reference SendGrid environment vars though
the backend now uses the "resend" package (see dependency "resend" in
package.json); update docs/contributing.md to replace SENDGRID_API_KEY and
SENDGRID_FROM_EMAIL with Resend equivalents (e.g., RESEND_API_KEY and
RESEND_FROM_EMAIL or RESEND_FROM_ADDRESS), update any example .env snippets and
instructions to show how to obtain and set RESEND_API_KEY, and ensure any code
references or CLI/env validation that mention SendGrid are updated to reference
the new Resend variable names (search for SENDGRID_API_KEY / SENDGRID_FROM_EMAIL
and replace with RESEND_*).
In `@docs/contributing.md`:
- Around line 248-249: The docs still reference SendGrid env vars but the code
uses Resend; update the contributing docs to reflect the actual Resend
environment variables (replace `SENDGRID_API_KEY` with `RESEND_API_KEY` and
`SENDGRID_FROM_EMAIL` with the corresponding `RESEND_FROM_EMAIL` or whatever
Resend sender env var your code expects), and ensure the description matches
(e.g., "Resend API key for sending verification emails" and "Email address used
as the Resend sender"); search for any other `SENDGRID_` mentions and align them
to the Resend variable names used by the implementation.
In `@frontend/app/_layout.tsx`:
- Around line 12-20: The current layout nests ConvexAuthProvider inside
ConvexProvider which prevents the auth provider from initializing correctly;
remove the outer ConvexProvider wrapper and use only ConvexAuthProvider with the
same client prop (convex) as the top-level provider around the app tree (keep
ConvexAuthProvider client={convex}, StatusBar, Stack and Stack.Screen entries
unchanged), ensuring you delete the ConvexProvider opening and closing tags and
any matching props so ConvexAuthProvider is the sole Convex provider.
🧹 Nitpick comments (6)
backend/convex/README.md (1)
4-4: Use markdown link syntax for URL.Per markdownlint, bare URLs should be wrapped in angle brackets or use proper markdown link syntax.
📝 Suggested fix
-See https://docs.convex.dev/functions for more. +See <https://docs.convex.dev/functions> for more.backend/convex/ResendOTP.ts (2)
12-15: Consider handling missing API key gracefully.If
AUTH_RESEND_KEYenvironment variable is not set,provider.apiKeywill beundefined, which may cause unclear errors when attempting to send emails. Consider adding validation or a clearer error message.♻️ Suggested improvement
export const ResendOTP = Email({ id: 'resend-otp', apiKey: process.env.AUTH_RESEND_KEY, maxAge: 60 * 15, // 15 minutesAlternatively, validate at send time:
async sendVerificationRequest({ identifier: email, provider, token }) { if (!provider.apiKey) { throw new ConvexError('Email service not configured. Please contact support.'); } // ... }
36-38: Production consideration: Configure sender email address.The
fromaddress is currently using Resend's defaultonboarding@resend.devdomain, which is suitable for development but should be updated for production. Consider making this configurable via environment variable.♻️ Suggested improvement
const resend = new ResendAPI(provider.apiKey); const { error } = await resend.emails.send({ - from: 'PolyBuys <onboarding@resend.dev>', + from: process.env.EMAIL_FROM || 'PolyBuys <onboarding@resend.dev>', to: [email],packages/shared/types/user.ts (1)
14-26: Consider usingAuthErrorCodetype forAuthError.code.The
AuthError.codeproperty is typed asstring | undefined, but you've defined a specificAuthErrorCodeunion type. Using the union type would provide better type safety.♻️ Suggested improvement
export interface AuthError { message: string; - code?: string; + code?: AuthErrorCode; }frontend/app/auth/login.tsx (1)
77-94: Add success feedback when code is resent.When
handleResendCodesucceeds, the code is cleared (line 87) but there's no visual feedback to the user that a new code was sent. Consider adding a success state or toast notification.♻️ Suggested improvement
+ const [successMessage, setSuccessMessage] = useState<string | null>(null); const handleResendCode = async () => { if (typeof step === 'string') return; setIsLoading(true); setError(null); + setSuccessMessage(null); try { await signIn('resend-otp', { email: step.email }); - setError(null); - // Show success feedback setCode(''); + setSuccessMessage('A new code has been sent to your email'); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to resend code'; setError(errorMessage); } finally { setIsLoading(false); } };Then display the success message in the UI similar to the error container.
frontend/hooks/useAuth.ts (1)
21-26: Use"skip"instead ofskipTokento conditionally disable the query when unauthenticated.
Convex React supports passing the string"skip"as query arguments to prevent backend calls and auth errors. UpdateisLoadingto not staytruewhen the query is intentionally skipped.♻️ Recommended adjustment
- const user = useQuery(api.users.getCurrentUser); - const isLoading = authLoading || user === undefined; + const user = useQuery( + api.users.getCurrentUser, + isAuthenticated ? undefined : "skip" + ); + const isLoading = authLoading || (isAuthenticated && user === undefined);
| "@polybuys/shared": "*", | ||
| "@convex-dev/auth": "^0.0.90", | ||
| "resend": "^4.0.0", | ||
| "@oslojs/crypto": "^1.0.0" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for SendGrid references in code and documentation
echo "=== SendGrid references in TypeScript/JavaScript ==="
rg -i 'sendgrid' --type ts --type tsx --type js 2>/dev/null || echo "No results found"
echo -e "\n=== SendGrid references in markdown files ==="
rg -i 'sendgrid' -g '*.md' 2>/dev/null || echo "No results found"
echo -e "\n=== Checking docs/contributing.md lines 248-249 specifically ==="
if [ -f "docs/contributing.md" ]; then
sed -n '240,260p' docs/contributing.md
else
echo "docs/contributing.md not found"
fiRepository: codebox-calpoly/PolyBuys
Length of output: 1527
Update environment variable documentation to reflect Resend instead of SendGrid.
The backend has been refactored to use the resend package for email delivery, but docs/contributing.md still documents SENDGRID_API_KEY and SENDGRID_FROM_EMAIL environment variables. Update the documentation to reflect the actual Resend-based configuration (e.g., RESEND_API_KEY).
🤖 Prompt for AI Agents
In `@backend/package.json` around lines 13 - 16, The docs still reference SendGrid
environment vars though the backend now uses the "resend" package (see
dependency "resend" in package.json); update docs/contributing.md to replace
SENDGRID_API_KEY and SENDGRID_FROM_EMAIL with Resend equivalents (e.g.,
RESEND_API_KEY and RESEND_FROM_EMAIL or RESEND_FROM_ADDRESS), update any example
.env snippets and instructions to show how to obtain and set RESEND_API_KEY, and
ensure any code references or CLI/env validation that mention SendGrid are
updated to reference the new Resend variable names (search for SENDGRID_API_KEY
/ SENDGRID_FROM_EMAIL and replace with RESEND_*).
| - `SENDGRID_API_KEY` - SendGrid API key for sending verification emails (get from tech leads) | ||
| - `SENDGRID_FROM_EMAIL` - Email address for sending verification emails (not set) |
There was a problem hiding this comment.
Outdated: Documentation references SendGrid but code uses Resend.
Per the commit history, the implementation was refactored to use Resend instead of SendGrid. These environment variable names should be updated to match the actual Resend configuration (likely RESEND_API_KEY).
📝 Suggested fix
-- `SENDGRID_API_KEY` - SendGrid API key for sending verification emails (get from tech leads)
-- `SENDGRID_FROM_EMAIL` - Email address for sending verification emails (not set)
+- `RESEND_API_KEY` - Resend API key for sending verification emails (get from tech leads)🤖 Prompt for AI Agents
In `@docs/contributing.md` around lines 248 - 249, The docs still reference
SendGrid env vars but the code uses Resend; update the contributing docs to
reflect the actual Resend environment variables (replace `SENDGRID_API_KEY` with
`RESEND_API_KEY` and `SENDGRID_FROM_EMAIL` with the corresponding
`RESEND_FROM_EMAIL` or whatever Resend sender env var your code expects), and
ensure the description matches (e.g., "Resend API key for sending verification
emails" and "Email address used as the Resend sender"); search for any other
`SENDGRID_` mentions and align them to the Resend variable names used by the
implementation.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@backend/convex/users.ts`:
- Around line 22-26: getCurrentUser is querying the users table using
authUser.email without normalizing case, which is inconsistent with
checkEmailExists/getOrCreateUser; modify the lookup in getCurrentUser so it
lowercases the email (e.g., const email = authUser.email.toLowerCase()) before
calling ctx.db.query('users').withIndex('by_email', (q) => q.eq('email',
email)).first() to ensure consistent, case-insensitive lookups.
- Around line 65-68: Normalize the email before querying and remove the
unnecessary non-null assertion: when building the users query that assigns
userProfile (the ctx.db.query('users').withIndex('by_email', (q) =>
q.eq('email', authUser.email!)).first()), replace authUser.email! with the same
normalized value used by getCurrentUser/getOrCreateUser (e.g., call the
project's normalizeEmail helper or apply the same trim()/toLowerCase() logic to
authUser.email) and drop the trailing ! since authUser.email is already guarded
earlier.
🧹 Nitpick comments (2)
backend/tsconfig.json (1)
13-14: Consider removing the redundantoutDir.With
noEmit: true, TypeScript won't emit any files, makingoutDir: "./dist"ineffective. You could remove it for clarity, though keeping it is harmless if you ever need to togglenoEmitoff for debugging.♻️ Optional cleanup
"resolveJsonModule": true, "allowSyntheticDefaultImports": true, - "outDir": "./dist", "noEmit": true,backend/convex/ResendOTP.ts (1)
41-42: Consider making the sender address configurable.The
fromaddress is hardcoded to'PolyBuys <onboarding@resend.dev>', which is Resend's sandbox domain. This works for development but will need a verified domain for production. Consider using an environment variable (e.g.,AUTH_RESEND_FROM_EMAIL) to make this configurable.♻️ Suggested refactor
const resend = new ResendAPI(provider.apiKey); + const fromEmail = process.env.AUTH_RESEND_FROM_EMAIL || 'PolyBuys <onboarding@resend.dev>'; const { error } = await resend.emails.send({ - from: 'PolyBuys <onboarding@resend.dev>', + from: fromEmail, to: [email],
Linked Issues
Closes #POLY-7
Linear: POLY-7
Summary
Implements user authentication with Cal Poly email verification. Users can sign up with email/password, and only
@calpoly.eduaddresses are accepted. Email verification is required before users can log in. Includes signup, login, and email verification screens with error handling.Note on Email Provider: This implementation uses SendGrid for sending verification emails. The email service is abstracted in backend/convex/emailActions.ts, so switching providers should be straightforward. In dev mode (without SendGrid configured), verification codes are logged to the console so you can test the full flow without setting up email.
Changes Made
Backend
userstable to schema with email, emailVerified, name, createdAtverificationTokenstable for secure token validationFrontend
Shared
Dependencies
@convex-dev/auth,@sendgrid/mail@convex-dev/authHow to Test
Option 1: Dev Mode (No Email Setup Required)
npm install- Install dependenciesnpm run dev:backend(in terminal A) - Watch console for verification codesnpm run dev(in terminal B)Option 2: With SendGrid
Add to backend/.env.local:
SENDGRID_API_KEY=''
SENDGRID_FROM_EMAIL=''
APP_URL='https://polybuys.app/'
Checklist
npm run lint)devQuestions for Review
Summary by CodeRabbit
New Features
Documentation
Quality of Life
✏️ Tip: You can customize this high-level summary in your review settings.