Adding Login Options
Enable Google, GitHub, email, or custom OAuth providers
BetterAuth supports multiple authentication providers through a consistent configuration pattern. StarterApp includes Google OAuth by default and can be extended with additional providers.
Provider Configuration
OAuth providers are enabled through environment variables. The computeSocialProviders function validates credentials and configures providers conditionally based on available configuration.
Supported Providers
BetterAuth includes built-in support for:
Google OAuth
Pre-configured in StarterApp with environment-based setup
GitHub OAuth
Add with GitHub OAuth App credentials
Microsoft Azure AD
Enterprise authentication with Azure Active Directory
Email/Password
Traditional credentials with bcrypt hashing
Magic Links
Passwordless email-based authentication
Custom OAuth2
Any OAuth2-compatible provider
Google OAuth Configuration
Google OAuth is enabled when environment variables are set in convex/lib/auth.ts
:
Environment Variables
Add to .env.local
:
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
DASHBOARD_BASE_URL=http://localhost:3000
Production URLs
In production, DASHBOARD_BASE_URL
must be https://
and match your OAuth redirect URI configuration in Google Cloud Console.
Google Cloud Console Setup
Create OAuth Client
- Go to Google Cloud Console
- Navigate to APIs & Services → Credentials
- Click "Create Credentials" → "OAuth client ID"
- Select "Web application"
Configure Redirect URIs
Add these authorized redirect URIs:
http://localhost:3000/api/auth/callback/google
https://your-domain.com/api/auth/callback/google
Replace your-domain.com
with your production domain.
Copy Credentials
Copy the Client ID and Client Secret to your .env.local
file.
Implementation Details
The computeSocialProviders
function in convex/lib/auth.ts
conditionally enables Google OAuth:
export function computeSocialProviders(env: Record<string, string | undefined>) {
const id = env.GOOGLE_CLIENT_ID?.trim();
const secret = env.GOOGLE_CLIENT_SECRET?.trim();
// In production, require both credentials if OAuth is expected
const isProduction =
env.CONVEX_CLOUD_URL?.includes(".convex.cloud") ||
env.NODE_ENV === "production";
if (isProduction && !(id && secret)) {
throw new Error("GOOGLE_CLIENT_ID/SECRET missing in production");
}
if (id && secret) {
return {
google: {
clientId: id,
clientSecret: secret,
},
};
}
return;
}
This function:
- Validates credentials are present in production
- Trims whitespace to prevent configuration errors
- Returns provider config only when both values exist
Adding GitHub OAuth
To add GitHub OAuth, modify convex/lib/auth.ts
:
1. Update Environment Variables
Add to .env.local
:
GITHUB_CLIENT_ID=your-github-oauth-app-id
GITHUB_CLIENT_SECRET=your-github-oauth-app-secret
2. Modify computeSocialProviders
export function computeSocialProviders(env: Record<string, string | undefined>) {
const googleId = env.GOOGLE_CLIENT_ID?.trim();
const googleSecret = env.GOOGLE_CLIENT_SECRET?.trim();
const githubId = env.GITHUB_CLIENT_ID?.trim();
const githubSecret = env.GITHUB_CLIENT_SECRET?.trim();
const isProduction =
env.CONVEX_CLOUD_URL?.includes(".convex.cloud") ||
env.NODE_ENV === "production";
const providers: Record<string, { clientId: string; clientSecret: string }> = {};
if (googleId && googleSecret) {
providers.google = {
clientId: googleId,
clientSecret: googleSecret,
};
}
if (githubId && githubSecret) {
providers.github = {
clientId: githubId,
clientSecret: githubSecret,
};
}
if (isProduction && Object.keys(providers).length === 0) {
throw new Error("At least one OAuth provider required in production");
}
return Object.keys(providers).length > 0 ? providers : undefined;
}
3. GitHub OAuth App Setup
Create OAuth App
- Go to GitHub Settings → Developer settings → OAuth Apps
- Click "New OAuth App"
- Fill in application details
Configure Callback URL
Set the authorization callback URL:
http://localhost:3000/api/auth/callback/github
https://your-domain.com/api/auth/callback/github
Copy Credentials
Copy the Client ID and generate a Client Secret.
Adding Email/Password Authentication
BetterAuth supports email/password authentication with bcrypt hashing:
1. Enable Email Provider
Modify createAuth
in convex/lib/auth.ts
:
export const createAuth = (ctx: ConvexGenericCtx) => {
const baseURL = computeBaseURL(process.env);
const socialProviders = computeSocialProviders(process.env);
return betterAuth({
baseURL,
database: convexAdapter(ctx, betterAuthComponent),
session: {
expiresIn: 60 * 60 * 24 * 7,
updateAge: 60 * 60 * 24,
cookie: sessionCookie,
},
plugins: [convex()],
...(socialProviders && { socialProviders }),
// Add email/password support
emailAndPassword: {
enabled: true,
minPasswordLength: 8,
maxPasswordLength: 128,
requireEmailVerification: true,
},
});
};
2. Add Sign-Up Form
Create a sign-up component in your client application:
"use client";
import { authClient } from "@workspace/auth/client";
import { useState } from "react";
export function SignUpForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
try {
await authClient.signUp.email({
email,
password,
name,
});
// Redirect or show success message
} catch (error) {
console.error("Sign up failed:", error);
}
};
return (
<form onSubmit={handleSignUp}>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Sign Up</button>
</form>
);
}
Adding Magic Link Authentication
Magic links provide passwordless authentication via email:
1. Enable Magic Links
Modify createAuth
in convex/lib/auth.ts
:
export const createAuth = (ctx: ConvexGenericCtx) => {
return betterAuth({
// ... existing config
magicLink: {
enabled: true,
expiresIn: 60 * 15, // 15 minutes
sendMagicLink: async ({ email, url }) => {
// Send email with magic link URL
// Integrate with your email service (Resend, SendGrid, etc.)
await sendEmail({
to: email,
subject: "Sign in to your account",
html: `<a href="${url}">Click here to sign in</a>`,
});
},
},
});
};
2. Create Magic Link Form
"use client";
import { authClient } from "@workspace/auth/client";
import { useState } from "react";
export function MagicLinkForm() {
const [email, setEmail] = useState("");
const [sent, setSent] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await authClient.signIn.magicLink({ email });
setSent(true);
} catch (error) {
console.error("Magic link failed:", error);
}
};
if (sent) {
return <div>Check your email for a sign-in link!</div>;
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Send Magic Link</button>
</form>
);
}
Security Features
All authentication providers include comprehensive security by default:
Testing Authentication
After adding a provider, test the complete flow:
Sign In
Navigate to /sign-in
and test authentication with your new provider.
Verify Session
Check that useSession()
returns the expected user data:
const { data: session } = useSession();
console.log(session?.user);
Test Protected Routes
Verify that protected routes redirect unauthenticated users:
const user = await requireUser(); // Should redirect to /sign-in when not authenticated
Check Convex Integration
Ensure Convex queries receive authentication:
const user = useQuery(api.auth.getCurrentUser);
// Should return user data when authenticated, null when not
Environment Variable Reference
Prop
Type
Production Requirements
In production, all OAuth credentials must be present and DASHBOARD_BASE_URL
must use https://
. Missing credentials throw an error to prevent misconfiguration.