Skip to main content

Overview

A full-featured password authentication system built on Convex Auth. Includes:
  • Email/password login and sign-up
  • Email verification with OTP codes
  • Password reset flow
  • Strong password requirements
  • Protected routes
  • Optional OAuth providers (GitHub, Google)

Installation

npx shadcn@latest add https://convex-ui.vercel.app/r/password-based-auth-nextjs
This installs:
  • Login, sign-up, and password reset forms
  • Logout button component
  • Protected route example
  • Complete Convex backend with auth
  • Convex client and provider

Post-Install Setup

1

Start Convex Development Server

Run the Convex dev server to initialize your deployment:
npx convex dev
This creates your Convex deployment and provides the deployment URL.
2

Configure Environment Variables

Add to your .env.local:
.env.local
NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
CONVEX_DEPLOYMENT=your-deployment-name
3

Set Up Email Provider

In the Convex dashboard (Settings > Environment Variables), add:
AUTH_RESEND_KEY=your-resend-api-key
AUTH_EMAIL_FROM=[email protected]  # optional
Get a free Resend API key at resend.com. This is required for email verification and password reset.
4

(Optional) Configure OAuth Providers

To enable GitHub or Google login, add to Convex dashboard:
AUTH_GITHUB_ID=your-github-client-id
AUTH_GITHUB_SECRET=your-github-client-secret

AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret
Configure callback URLs in provider dashboards:
  • GitHub: https://<your-convex-url>/api/auth/callback/github
  • Google: https://<your-convex-url>/api/auth/callback/google
5

Wrap App with Provider

Ensure your app is wrapped with the Convex provider:
app/layout.tsx
import { ConvexClientProvider } from "@/lib/convex/provider";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ConvexClientProvider>{children}</ConvexClientProvider>
      </body>
    </html>
  );
}

Components

LoginForm

Email and password login form with validation:
app/auth/login/page.tsx
import { LoginForm } from "@/components/login-form";

export default function LoginPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <LoginForm />
    </div>
  );
}
Features:
  • Email/password validation
  • Loading states
  • Error handling
  • Link to sign-up page
  • Link to forgot password

SignUpForm

User registration with email verification:
app/auth/sign-up/page.tsx
import { SignUpForm } from "@/components/sign-up-form";

export default function SignUpPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignUpForm />
    </div>
  );
}
Features:
  • Email validation
  • Strong password requirements
  • Email verification flow
  • OTP code entry

ForgotPasswordForm

Password reset request form:
app/auth/forgot-password/page.tsx
import { ForgotPasswordForm } from "@/components/forgot-password-form";

export default function ForgotPasswordPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <ForgotPasswordForm />
    </div>
  );
}

UpdatePasswordForm

Set new password with OTP verification:
app/auth/update-password/page.tsx
import { UpdatePasswordForm } from "@/components/update-password-form";

export default function UpdatePasswordPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <UpdatePasswordForm />
    </div>
  );
}

LogoutButton

Simple logout button:
import { LogoutButton } from "@/components/logout-button";

export function Header() {
  return (
    <header>
      <LogoutButton />
    </header>
  );
}

Authentication Hooks

useAuthActions

Handle authentication operations:
import { useAuthActions } from "@convex-dev/auth/react";

function MyComponent() {
  const { signIn, signOut } = useAuthActions();
  
  const handleLogin = async () => {
    const formData = new FormData();
    formData.set("email", "[email protected]");
    formData.set("password", "password123");
    formData.set("flow", "signIn");
    
    await signIn("password", formData);
  };
  
  return <button onClick={handleLogin}>Sign In</button>;
}

useCurrentUser

Access the current authenticated user:
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";

function UserProfile() {
  const user = useQuery(api.users.current);
  
  if (!user) return <div>Not logged in</div>;
  
  return <div>Welcome, {user.name}!</div>;
}

Backend Configuration

The backend is configured in convex/auth.ts:
convex/auth.ts
import { convexAuth } from "@convex-dev/auth/server";
import { Password } from "@convex-dev/auth/providers/Password";
import Resend from "@auth/core/providers/resend";
import GitHub from "@auth/core/providers/github";
import Google from "@auth/core/providers/google";

// Custom password provider with email verification
const CustomPassword = Password({
  verify: ResendOTP,
  reset: ResendOTPPasswordReset,
  validatePasswordRequirements: (password: string) => {
    if (password.length < 8) {
      throw new Error("Password must be at least 8 characters");
    }
    if (!/[a-z]/.test(password)) {
      throw new Error("Password must contain a lowercase letter");
    }
    if (!/[A-Z]/.test(password)) {
      throw new Error("Password must contain an uppercase letter");
    }
    if (!/[0-9]/.test(password)) {
      throw new Error("Password must contain a number");
    }
  },
});

export const { auth, signIn, signOut } = convexAuth({
  providers: [CustomPassword, GitHub, Google],
});

Password Requirements

Passwords must contain:
  • Minimum 8 characters
  • At least one lowercase letter
  • At least one uppercase letter
  • At least one number

Protected Routes

Protect routes using server-side checks:
app/protected/page.tsx
import { redirect } from "next/navigation";
import { fetchQuery } from "@/lib/convex/server";
import { api } from "@/convex/_generated/api";

export default async function ProtectedPage() {
  const user = await fetchQuery(api.users.current);
  
  if (!user) {
    redirect("/auth/login");
  }
  
  return <div>Protected content</div>;
}
Or use client-side protection:
"use client";

import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import { redirect } from "next/navigation";

export default function ProtectedPage() {
  const user = useQuery(api.users.current);
  
  if (user === undefined) return <div>Loading...</div>;
  if (user === null) redirect("/auth/login");
  
  return <div>Protected content</div>;
}

Environment Variables

Client (.env.local)

NEXT_PUBLIC_CONVEX_URL
string
required
Your Convex deployment URL
CONVEX_DEPLOYMENT
string
required
Your Convex deployment name

Server (Convex Dashboard)

AUTH_RESEND_KEY
string
required
Resend API key for sending email verification and reset codes
AUTH_EMAIL_FROM
string
Sender email address. Defaults to [email protected]
AUTH_GITHUB_ID
string
GitHub OAuth client ID (optional)
AUTH_GITHUB_SECRET
string
GitHub OAuth client secret (optional)
AUTH_GOOGLE_ID
string
Google OAuth client ID (optional)
AUTH_GOOGLE_SECRET
string
Google OAuth client secret (optional)

Email Verification Flow

  1. User signs up with email and password
  2. System sends 8-digit OTP to user’s email
  3. User enters OTP code
  4. Account is verified and user is logged in
OTP codes expire after 15 minutes.

Password Reset Flow

  1. User requests password reset
  2. System sends 8-digit OTP to user’s email
  3. User enters OTP and new password
  4. Password is updated and user is logged in

Build docs developers (and LLMs) love