Skip to content

feat: Implement user auth with Cal Poly email verification#21

Merged
jaydonkc merged 8 commits intodevfrom
feature/POLY-7-cal-poly-email-auth
Jan 23, 2026
Merged

feat: Implement user auth with Cal Poly email verification#21
jaydonkc merged 8 commits intodevfrom
feature/POLY-7-cal-poly-email-auth

Conversation

@cole-hackman
Copy link
Copy Markdown
Collaborator

@cole-hackman cole-hackman commented Jan 19, 2026

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.edu addresses 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

  • Added users table to schema with email, emailVerified, name, createdAt
  • Added verificationTokens table for secure token validation
  • Created auth.ts with Convex Auth Password provider configuration
  • Created http.ts for Convex Auth HTTP routes
  • Created emailActions.ts with SendGrid integration (dev mode fallback logs to console)
  • Created verification.ts for token storage/deletion mutations
  • Created users.ts with queries/mutations for user management
  • Added email domain validation (enforces @calpoly.edu)
  • Security: Verification tokens are validated server-side before marking email as verified

Frontend

  • Created useAuth.ts hook wrapping Convex Auth React hooks
  • Created signup screen (auth/signup.tsx) with form validation
  • Created login screen (auth/login.tsx) with error handling
  • Created email verification screen (auth/verify-email.tsx) with resend functionality
  • Updated _layout.tsx to include AuthProvider and auth routes

Shared

  • Updated User type to match schema
  • Enhanced isCalPolyEmail utility with regex validation
  • Added getEmailValidationError helper function

Dependencies

  • Backend: @convex-dev/auth, @sendgrid/mail
  • Frontend: @convex-dev/auth

How to Test

Option 1: Dev Mode (No Email Setup Required)

  1. npm install - Install dependencies
  2. npm run dev:backend (in terminal A) - Watch console for verification codes
  3. npm run dev (in terminal B)
  4. Navigate to signup screen and create account
  5. Check terminal A console output for the verification code
  6. Enter code on verify-email screen

Option 2: With SendGrid
Add to backend/.env.local:
SENDGRID_API_KEY=''
SENDGRID_FROM_EMAIL=''
APP_URL='https://polybuys.app/'

Checklist

  • Tests added/updated (if applicable)
  • Lint/tests pass locally (npm run lint)
  • Docs updated (README/ADR/changelog if needed)
  • Follows conventional commit format
  • No merge conflicts with dev

Questions for Review

  1. Email Provider: Should we use SendGrid or consider alternatives (Resend, MailerLite, AWS SES, etc)?
  2. Sender Email: What email address should we use for sending verification emails?

Summary by CodeRabbit

  • New Features

    • Email-based OTP sign-in for Cal Poly addresses with resend support
    • Two-step Sign In screen and auth-aware app layout; app title updated to "PolyBuys"
    • Backend auth integration and user profile flows: create, fetch, update, sign-out
  • Documentation

    • Expanded environment variable docs and rewritten backend auth guide with example-driven client usage
  • Quality of Life

    • Stronger email validation and new auth error types/messages

✏️ Tip: You can customize this high-level summary in your review settings.

- 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
@cole-hackman cole-hackman self-assigned this Jan 19, 2026
Copy link
Copy Markdown
Collaborator

@evan-taylor evan-taylor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I think the implementation is super solid! A few configuration things I added!

Comment thread backend/convex/emailActions.ts Outdated
Comment thread backend/convex/emailActions.ts Outdated
Comment thread backend/convex/auth.ts Outdated
- 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
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Convex docs & configs
backend/convex/README.md, backend/convex/tsconfig.json, backend/tsconfig.json
README rewritten to examples-first; removed empty types entry and included __tests__; backend tsconfig switched module/resolution and set noEmit.
Auth core & HTTP router
backend/convex/auth.ts, backend/convex/http.ts
Adds convexAuth initialization (exports auth, signIn, signOut, store, isAuthenticated) and registers auth HTTP routes on a new http router.
OTP email provider
backend/convex/ResendOTP.ts
New ResendOTP provider: crypto-backed 8-digit OTP generation, Cal Poly email validation, sends plain/HTML email via Resend SDK, validates API key, and surfaces errors as ConvexError.
Users schema & APIs
backend/convex/schema.ts, backend/convex/users.ts
Adds users table (email, name, createdAt) with by_email index; new queries/mutations: getCurrentUser, checkEmailExists, updateUserProfile, getOrCreateUser with auth checks and index-backed lookups.
Frontend auth UI & hook
frontend/app/_layout.tsx, frontend/app/auth/login.tsx, frontend/hooks/useAuth.ts
Replaces ConvexProvider with ConvexAuthProvider; adds auth/login screen (email → OTP flow, resend/back); introduces useAuth hook returning user, isAuthenticated, isLoading, signOut.
Env docs & examples
frontend/.env.example, docs/contributing.md, backend/convex/README.md
Removes self-hosted example env lines; reorganizes env docs into Frontend/Backend and adds AUTH_RESEND_KEY; README updated to function-first usage examples.
Dependencies & shared types/utils
backend/package.json, frontend/package.json, packages/shared/types/user.ts, packages/shared/utils/index.ts, packages/shared/package.json
Adds @convex-dev/auth, resend, @oslojs/crypto; adds "type":"module" entries; updates User.name → `string

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped a note from code to mail,
An 8-digit nibble on a Cal Poly trail.
Convex hums while Resend sends light,
Users sign in through the velvet night.
Hooray—PolyBuys leaps into sight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature being implemented: user authentication with Cal Poly email verification, which aligns with the primary focus of all changes across backend, frontend, and shared code.
Description check ✅ Passed The description is comprehensive and follows the template structure with all major sections completed: linked issues, detailed summary, explicit changes breakdown by layer, testing instructions (with dev and SendGrid options), full checklist with items marked, and reviewer questions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_KEY environment variable is not set, provider.apiKey will be undefined, 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 minutes

Alternatively, 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 from address is currently using Resend's default onboarding@resend.dev domain, 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 using AuthErrorCode type for AuthError.code.

The AuthError.code property is typed as string | undefined, but you've defined a specific AuthErrorCode union 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 handleResendCode succeeds, 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 of skipToken to conditionally disable the query when unauthenticated.
Convex React supports passing the string "skip" as query arguments to prevent backend calls and auth errors. Update isLoading to not stay true when 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);

Comment thread backend/convex/users.ts
Comment thread backend/package.json
Comment on lines +13 to +16
"@polybuys/shared": "*",
"@convex-dev/auth": "^0.0.90",
"resend": "^4.0.0",
"@oslojs/crypto": "^1.0.0"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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"
fi

Repository: 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_*).

Comment thread docs/contributing.md Outdated
Comment on lines +248 to +249
- `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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment thread frontend/app/_layout.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 redundant outDir.

With noEmit: true, TypeScript won't emit any files, making outDir: "./dist" ineffective. You could remove it for clarity, though keeping it is harmless if you ever need to toggle noEmit off 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 from address 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],

Comment thread backend/convex/users.ts
Comment thread backend/convex/users.ts
Comment thread frontend/app/_layout.tsx Outdated
@jaydonkc jaydonkc merged commit 81b17e1 into dev Jan 23, 2026
3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Feb 11, 2026
5 tasks
@mattphanm mattphanm mentioned this pull request Feb 16, 2026
5 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Mar 11, 2026
5 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Apr 16, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants