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:
- Resend API Key: For sending password reset emails
- Google OAuth: Client ID and Secret from Google Console
- 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:
- Convex Auth Documentation - Official authentication guide
- Auth Setup - Initial configuration steps
- Auth Providers - Available authentication providers
- Next.js Integration - Next.js-specific setup guide
- Custom Providers - Creating custom auth providers
🚀 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