StarterApp Docs
Authentication

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

  1. Go to Google Cloud Console
  2. Navigate to APIs & Services → Credentials
  3. Click "Create Credentials" → "OAuth client ID"
  4. 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

  1. Go to GitHub Settings → Developer settings → OAuth Apps
  2. Click "New OAuth App"
  3. 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>
  );
}

Magic links provide passwordless authentication via email:

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>`,
        });
      },
    },
  });
};
"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.