Skip to Content
Iris Saas Kit documentation is under construction.
TechnicalConvex Auth

Convex Auth

Complete authentication system using @convex-dev/auth with OAuth and password providers.

🔧 Backend Authentication Setup

Main Configuration

Core auth setup in convex/auth.ts:

// convex/auth.ts import Google from '@auth/core/providers/google'; import Resend from '@auth/core/providers/resend'; import { alphabet, generateRandomString } from 'oslo/crypto'; import { Resend as ResendAPI } from 'resend'; import { Password } from '@convex-dev/auth/providers/Password'; import { convexAuth } from '@convex-dev/auth/server'; import ResetPasswordEmail from './_emailTemplates/ResetPassword'; import { internal } from './_generated/api'; import { MutationCtx } from './_generated/server'; import { AUTH_GOOGLE_CLIENT_ID, AUTH_GOOGLE_CLIENT_SECRET, AUTH_RESEND_KEY, SITE_URL, } from './envs'; export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ providers: [ Password({ reset: ResendOTPPasswordReset, }), Google({ clientId: AUTH_GOOGLE_CLIENT_ID, clientSecret: AUTH_GOOGLE_CLIENT_SECRET, }), ], callbacks: { createOrUpdateUser: async (ctx: MutationCtx, args) => { if (args.existingUserId) { return args.existingUserId; } // Check for existing user by email const existingUser = await ctx.db .query('users') .withIndex('email', q => q.eq('email', args.profile.email!)) .first(); if (existingUser) return existingUser._id; // Create new user const userId = await ctx.db.insert('users', { email: args.profile.email!, name: args.profile.email!.split('@')[0], }); // Send admin notification await ctx.scheduler.runAfter( 0, internal.core.devAlertsNode.sendNotification, { text: `🆕 New sign-up: *${args.profile.email}*`, channel: 'general', } ); return userId; }, }, });

Key Features:

  • Multiple providers: Password + Google OAuth
  • Custom password reset: Using Resend with email templates
  • Account linking: Automatic user creation/linking by email
  • Admin notifications: Real-time alerts for new signups

Password Reset with Email

Custom Resend configuration for password reset:

// Custom OTP-based password reset const ResendOTPPasswordReset = Resend({ id: 'resend-otp', apiKey: AUTH_RESEND_KEY, async generateVerificationToken() { return generateRandomString(8, alphabet('0-9')); // 8-digit code }, async sendVerificationRequest({ identifier: email, provider, token }) { const resend = new ResendAPI(provider.apiKey); const { error } = await resend.emails.send({ from: 'dev@iristech.my', to: [email], subject: `Sign in to Iris Saas Kit`, react: ResetPasswordEmail({ email, url: `${SITE_URL}/reset-password?token=${token}`, }), }); if (error) { throw new Error(`Could not send ${JSON.stringify(error)}`); } }, });

Environment Variables

Required environment variables in convex/envs.ts:

// convex/envs.ts export const CONVEX_SITE_URL = process.env.CONVEX_SITE_URL!; // Convex deployment URL export const SITE_URL = process.env.SITE_URL!; // Frontend URL // Email export const AUTH_RESEND_KEY = process.env.AUTH_RESEND_KEY!; export const EMAIL_DOMAIN = process.env.EMAIL_DOMAIN!; // e.g. @mysite.com // Google OAuth export const AUTH_GOOGLE_CLIENT_ID = process.env.AUTH_GOOGLE_CLIENT_ID!; export const AUTH_GOOGLE_CLIENT_SECRET = process.env.AUTH_GOOGLE_CLIENT_SECRET!;

Environment setup:

  1. Resend API Key: For sending password reset emails
  2. Google OAuth: Client ID and Secret from Google Console
  3. Site URLs: Frontend and Convex deployment URLs

🛠️ Frontend Implementation

Authentication Providers

Root layout with server-side auth provider:

// app/layout.tsx import { ConvexAuthNextjsServerProvider } from "@convex-dev/auth/nextjs/server"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <ConvexAuthNextjsServerProvider> <html lang="en" suppressHydrationWarning> <body> <ThemeProvider> {children} </ThemeProvider> </body> </html> </ConvexAuthNextjsServerProvider> ); }

Client-side provider for authenticated routes:

// components/ConvexClientProvider.tsx import { ConvexAuthNextjsProvider } from "@convex-dev/auth/nextjs"; export function ConvexAuthClientProvider({ children, }: { children: ReactNode; }) { return ( <ConvexAuthNextjsProvider client={convex}> {children} </ConvexAuthNextjsProvider> ); }

Sign In Implementation

Complete sign in page with OAuth and password options:

// app/(public-with-convex-auth-client)/signin/page.tsx "use client"; import { useAuthActions } from "@convex-dev/auth/react"; export default function SignIn() { const { signIn } = useAuthActions(); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); // Password sign in const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); setLoading(true); setError(null); const formData = new FormData(event.currentTarget); const email = formData.get("email") as string; const password = formData.get("password") as string; if (!email || !password) { setError("Email and password are required."); setLoading(false); return; } try { await signIn("password", formData); } catch (err) { setError("Invalid email or password. Please try again."); } finally { setLoading(false); } }; // Google OAuth sign in const handleGoogleSignIn = async () => { setLoading(true); setError(null); try { await signIn("google"); } catch (err) { setError("Failed to sign in with Google. Please try again."); } finally { setLoading(false); } }; return ( <div className="min-h-screen flex items-center justify-center"> <Card> <CardContent className="pt-6"> {/* Google Sign In */} <Button type="button" onClick={handleGoogleSignIn} variant="outline" className="w-full h-11" disabled={loading} > <Image src="/google-logo.svg" alt="Google" width={20} height={20} className="mr-2" /> Continue with Google </Button> <Separator className="my-6" /> {/* Email/Password Form */} <form onSubmit={handleSubmit} className="space-y-4"> <Input name="email" type="email" placeholder="Enter your email" required className="h-11" /> <Input name="password" type="password" placeholder="Enter your password" required className="h-11" /> <input name="flow" type="hidden" value="signIn" /> {error && ( <div className="p-3 rounded-md bg-destructive/10"> <p className="text-sm text-destructive">{error}</p> </div> )} <Button type="submit" className="w-full h-11" disabled={loading}> {loading ? "Signing in..." : "Sign in"} </Button> </form> </CardContent> </Card> </div> ); }

Sign Up Implementation

User registration with validation:

// app/(public-with-convex-auth-client)/signup/page.tsx export default function SignUp() { const { signIn } = useAuthActions(); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); setIsLoading(true); setError(null); const formData = new FormData(event.currentTarget as HTMLFormElement); // Password validation if (password !== confirmPassword) { setError("Passwords do not match!"); setIsLoading(false); return; } if (password.length < 6) { setError("Password must be at least 6 characters long"); setIsLoading(false); return; } try { formData.set("redirectTo", "/onboarding"); await signIn("password", formData); } catch (err) { setError("Failed to create account. Please try again."); } finally { setIsLoading(false); } }; return ( <form onSubmit={handleSubmit}> <Input name="email" type="email" required /> <Input name="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required /> <Input name="confirmPassword" type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} required /> <input name="flow" type="hidden" value="signUp" /> <Button type="submit" disabled={isLoading}> {isLoading ? "Creating account..." : "Create account"} </Button> </form> ); }

Password Reset Flow

Complete password reset with email verification:

// app/(public-with-convex-auth-client)/reset-password/page.tsx export default function ResetPassword() { const { signIn } = useAuthActions(); const router = useRouter(); const handleResetPassword = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); try { setIsLoading(true); const formData = new FormData(); formData.append("email", email); formData.append("flow", "reset"); // Request password reset await signIn("password", formData); toast.success("Check your email for the code."); router.push(`/create-new-password?email=${encodeURIComponent(email)}`); } catch (error: any) { const errorMessage = error?.message || error?.toString() || ""; if (errorMessage.includes("InvalidAccountId") || errorMessage.includes("not found")) { setError("No account found with this email. Please sign up first."); } else { setError("Failed to send reset request. Please try again."); } } finally { setIsLoading(false); } }; return ( <form onSubmit={handleResetPassword}> <Input name="email" type="email" placeholder="james@yahoo.com" required /> <Button type="submit" disabled={isLoading}> {isLoading ? "Sending..." : "Send reset code"} </Button> </form> ); }

🔒 Route Protection

Middleware Configuration

Authentication middleware for route protection:

// middleware.ts import { convexAuthNextjsMiddleware, createRouteMatcher, nextjsMiddlewareRedirect, } from '@convex-dev/auth/nextjs/server'; const isSignInPage = createRouteMatcher(['/signin']); const isSignUpPage = createRouteMatcher(['/signup']); export default convexAuthNextjsMiddleware( async (request, { convexAuth }) => { if (isSignInPage(request) && (await convexAuth.isAuthenticated())) { return nextjsMiddlewareRedirect(request, '/workspaces'); } else if (isSignUpPage(request) && (await convexAuth.isAuthenticated())) { return nextjsMiddlewareRedirect(request, '/onboarding'); } }, { verbose: true } ); export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], };

Key Features:

  • Automatic redirects: Authenticated users redirected from auth pages
  • Global middleware: Runs on all routes except static assets
  • Flexible routing: Support for different post-auth destinations

Layout-Level Protection

Server-side authentication in protected layouts:

// app/(authenticated)/layout.tsx import { redirect } from "next/navigation"; import { isAuthenticatedNextjs } from "@convex-dev/auth/nextjs/server"; import { ConvexAuthClientProvider } from "@/components/ConvexClientProvider"; const AuthenticatedLayout = async ({ children }: Props) => { const isAuthenticated = await isAuthenticatedNextjs(); if (!isAuthenticated) { redirect("/signin"); } return <ConvexAuthClientProvider>{children}</ConvexAuthClientProvider>; };

Benefits:

  • Server-side validation: Authentication checked before page render
  • Automatic redirects: Unauthenticated users sent to sign-in
  • Provider context: Authenticated pages get Convex client with auth

📧 Email Templates

Password Reset Email

React Email template with Tailwind styling:

// convex/_emailTemplates/ResetPassword.tsx import { Body, Button, Container, Head, Heading, Hr, Html, Img, Link, Section, Tailwind, Text, } from "@react-email/components"; export const ResetPasswordEmail = (props: { email: string; url: string }) => { const { email, url } = props; return ( <Html> <Head /> <Tailwind> <Body className="bg-white my-auto mx-auto font-sans px-2"> <Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]"> <Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0"> Reset your password on <strong>Iris Saas Kit</strong> </Heading> <Text className="text-black text-[14px] leading-[24px]"> We received a request to reset the password for your account. </Text> <Section className="text-center mt-[32px] mb-[32px]"> <Button style={{ backgroundColor: "#000", borderRadius: "4px", fontSize: "12px", fontWeight: "600", padding: "12px 20px", color: "#fff", }} href={url} > Reset password </Button> </Section> <Text className="text-black text-[14px] leading-[24px]"> or copy and paste this URL into your browser:{" "} <Link href={url} className="text-blue-600 no-underline"> {url} </Link> </Text> <Text className="text-[#666666] text-[12px] leading-[24px]"> This password reset was requested for{" "} <span className="text-black">{email}</span>. </Text> </Container> </Body> </Tailwind> </Html> ); };

🔗 Official Documentation

For complete setup instructions and advanced configuration:

🚀 Key Benefits

Production-Ready Features:

  • Multiple auth methods: OAuth + password with reset capability
  • Server-side validation: Authentication checked at the layout level
  • Automatic redirects: Smart routing based on auth status
  • Email integration: Beautiful transactional emails with React Email
  • Type safety: Full TypeScript integration throughout
  • Error handling: Comprehensive error states and user feedback

Developer Experience:

  • Easy setup: Minimal configuration required
  • Flexible routing: Support for different user flows
  • Custom callbacks: User creation and account linking logic
  • Real-time notifications: Admin alerts for new signups
  • Email templates: Reusable React Email components
Last updated on