Skip to main content
Enable Supabase Auth for your app built with Sabo. You’ll configure Supabase, set env vars, verify preconfigured clients, protect routes, implement sign-in/sign-up (incl. OAuth and Magic Link), and support password recovery.

Quick Setup

1

Set up Supabase (Dashboard)

Create your project and configure Auth settings:
  • Copy your Project URL and anon public key (Dashboard → Settings → API). You can start from the Supabase Dashboard.
  • Set your Site URL and add /auth/callback to Additional Redirect URLs for dev and prod (Dashboard → Auth → URL Configuration). Guide: Redirect URLs.
  • Optional: enable your social providers (e.g., Google, GitHub, Apple, Facebook) and use /auth/callback as the callback (Dashboard → Auth → Providers). See the full list: Social login. Example guide: Login with Google.
By default, Sabo uses a single /auth/callback to handle signup confirmation, password recovery, and OAuth code exchange in one place.
Multiple domains/apps? Add each origin to Additional Redirect URLs and expose a callback endpoint on every domain that needs to complete the code exchange. See Redirect URLs.
Confirm: env keys are set, /auth/callback is whitelisted for the domains you use, and (if enabled) providers redirect back successfully.
2

Define environment variables

Add these environment variables to your local .env (or .env.local) and to your hosting provider.
.env
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
SUPABASE_SECRET_KEY=
NEXT_PUBLIC_SITE_URL=http://localhost:3000
SUPABASE_SECRET_KEY is the server-only secret key (the modern replacement for the legacy service_role JWT). Never expose it to the browser—only server code such as webhooks or admin utilities should read it.
Run the app and ensure env vars are loaded (no undefined errors).
3

Verify preconfigured clients

Sabo already ships with multiple clients:
  • Browser client: src/lib/supabase/client.ts
  • Server client (cookies wired): src/lib/supabase/server.ts
You generally don’t need to modify these. Only change them if you’re customizing cookie strategy or adding admin utilities.
Run locally and ensure there are no missing env errors for NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY.
4

Route protection (summary)

Sabo applies a global Supabase auth middleware that:
  • keeps auth cookies in sync for SSR,
  • redirects signed-out users away from protected pages,
  • redirects signed-in users away from auth pages.
Customize protected/auth routes and matchers as needed.
For full patterns (matchers, includes/excludes, Stripe webhook exclusion, roles/tenants), see Routing & Middleware.

Authentication Flows

Sabo supports four main authentication flows:
  1. Email/Password - Traditional email and password sign-in/sign-up
  2. Magic Link - Passwordless authentication via email link
  3. OAuth - Social login with Google, GitHub, Apple
  4. Password Reset - Forgot password and reset flow
Each flow is fully implemented with server actions and UI pages.

Email/Password Flow

Sign In Flow

The email/password sign-in uses a two-step UX for better security and flexibility:
1

Step 1: Enter email (/sign-in)

User enters their email address on /sign-in page.
src/app/(auth)/sign-in/page.tsx
"use client";

export default function SignInPage() {
  const [email, setEmail] = useState("");
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    // Save email to sessionStorage
    sessionStorage.setItem("signin-email", email);
    
    // Navigate to confirm page
    router.push("/sign-in/confirm");
  };

  return (
    <form onSubmit={handleSubmit}>
      <Input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email address"
        required
      />
      <Button type="submit">Continue</Button>
    </form>
  );
}
What happens:
  • Email is validated (client-side)
  • Email is stored in sessionStorage for next step
  • User is redirected to /sign-in/confirm
2

Step 2: Choose authentication method (/sign-in/confirm)

User sees two options:
  1. Magic Link - Send a passwordless login link via email
  2. Password - Enter password to sign in
src/app/(auth)/sign-in/confirm/page.tsx
"use client";

import { signIn, signInWithMagicLink } from "../actions";

export default function SignInConfirmPage() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  useEffect(() => {
    // Get email from sessionStorage
    const savedEmail = sessionStorage.getItem("signin-email");
    if (!savedEmail) {
      router.push("/sign-in");
      return;
    }
    setEmail(savedEmail);
  }, []);

  const handlePasswordSignIn = async (e: React.FormEvent) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append("email", email);
    formData.append("password", password);
    
    const result = await signIn(formData);
    if (result?.error) {
      setError(result.error);
    }
    // Success: redirects to /dashboard
  };

  const handleMagicLinkSend = async () => {
    const result = await signInWithMagicLink(email);
    if (result?.error) {
      setError(result.error);
    } else {
      toast.success("Magic link sent!", {
        description: "Check your email to sign in."
      });
    }
  };

  return (
    <>
      <Input type="email" value={email} disabled />
      
      <Button onClick={handleMagicLinkSend}>
        Send magic link
      </Button>
      
      <Input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Your password"
      />
      <Button onClick={handlePasswordSignIn}>
        Sign in
      </Button>
    </>
  );
}
What happens:
  • Email is retrieved from sessionStorage and shown (disabled)
  • User can choose Magic Link OR password
  • Password sign-in → calls signIn() server action → redirects to /dashboard
  • Magic Link → calls signInWithMagicLink() → sends email → user waits for email
3

Server action: signIn()

src/app/(auth)/actions.ts
export async function signIn(formData: FormData, redirectTo?: string | null) {
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  const supabase = await createClient();

  const { error } = await supabase.auth.signInWithPassword({
    email,
    password,
  });

  if (error) {
    return { error: error.message };
  }

  revalidatePath("/", "layout");
  redirect(redirectTo || "/dashboard");
}
What happens:
  • Supabase verifies email/password
  • If successful: sets session cookie and redirects to /dashboard
  • If failed: returns error message
The two-step UX allows users to choose their preferred authentication method while keeping the UI clean and focused.
Need to customize confirmation, magic link, or password reset emails? Follow Auth Emails with Resend for template editing, SMTP setup, and deliverability guidance.

Sign Up Flow

Sign-up follows a similar two-step pattern:
1

Step 1: Enter name and email (/sign-up)

User enters full name and email address.
src/app/(auth)/sign-up/page.tsx
"use client";

export default function SignUpPage() {
  const [fullName, setFullName] = useState("");
  const [email, setEmail] = useState("");
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    // Save data to sessionStorage
    sessionStorage.setItem("signup-fullname", fullName);
    sessionStorage.setItem("signup-email", email);
    
    // Navigate to confirm page
    router.push("/sign-up/confirm");
  };

  return (
    <form onSubmit={handleSubmit}>
      <Input
        type="text"
        value={fullName}
        onChange={(e) => setFullName(e.target.value)}
        placeholder="Your name"
        required
      />
      <Input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email address"
        required
      />
      <Button type="submit">Continue</Button>
    </form>
  );
}
2

Step 2: Choose registration method (/sign-up/confirm)

User chooses:
  1. Magic Link - Complete registration via email link (no password needed)
  2. Password - Create a password-based account
src/app/(auth)/sign-up/confirm/page.tsx
"use client";

import { signUp, signInWithMagicLink } from "../actions";

export default function SignUpConfirmPage() {
  const [fullName, setFullName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  useEffect(() => {
    const savedFullName = sessionStorage.getItem("signup-fullname");
    const savedEmail = sessionStorage.getItem("signup-email");
    if (!savedFullName || !savedEmail) {
      router.push("/sign-up");
      return;
    }
    setFullName(savedFullName);
    setEmail(savedEmail);
  }, []);

  const handlePasswordSignUp = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (password.length < 8) {
      setError("Password must be at least 8 characters long.");
      return;
    }
    
    const formData = new FormData();
    formData.append("fullName", fullName);
    formData.append("email", email);
    formData.append("password", password);
    
    const result = await signUp(formData);
    if (result?.error) {
      setError(result.error);
    } else {
      router.push("/sign-in?signup=success");
    }
  };

  const handleMagicLinkSend = async () => {
    const result = await signInWithMagicLink(email);
    if (result?.error) {
      setError(result.error);
    } else {
      toast.success("Magic link sent!", {
        description: "Check your email to complete your registration."
      });
    }
  };

  return (
    <>
      <Input type="text" value={fullName} disabled />
      <Input type="email" value={email} disabled />
      
      <Button onClick={handleMagicLinkSend}>
        Continue with magic link
      </Button>
      
      <Input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Create a password"
      />
      <Button onClick={handlePasswordSignUp}>
        Create account
      </Button>
    </>
  );
}
3

Server action: signUp()

src/app/(auth)/actions.ts
export async function signUp(formData: FormData) {
  const fullName = formData.get("fullName") as string;
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  const supabase = await createClient();

  const { error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      data: {
        full_name: fullName,
      },
      emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
    },
  });

  if (error) {
    return { error: error.message };
  }

  revalidatePath("/", "layout");
  return { success: true };
}
What happens:
  • Supabase creates new user account
  • Sends confirmation email with link to /auth/callback
  • User must click email link to activate account
  • After activation: redirected to /dashboard
By default, Supabase requires email confirmation for new accounts. Users must click the link in their email before they can sign in.To disable email confirmation (not recommended for production), go to Supabase Dashboard → Authentication → Settings → Email Auth → Disable “Enable email confirmations”.

Magic Links provide passwordless authentication - users receive an email with a one-time login link.
1

User requests magic link

From either /sign-in/confirm or /sign-up/confirm, user clicks “Send magic link” button.
const handleMagicLinkSend = async () => {
  const result = await signInWithMagicLink(email);
  if (result?.error) {
    setError(result.error);
  } else {
    toast.success("Magic link sent!", {
      description: "Check your email to sign in."
    });
  }
};
2

Server action sends email

src/app/(auth)/actions.ts
export async function signInWithMagicLink(
  email: string,
  redirectTo?: string | null
) {
  const supabase = await createClient();

  const callbackUrl = redirectTo
    ? `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback?next=${encodeURIComponent(redirectTo)}`
    : `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`;

  const { error } = await supabase.auth.signInWithOtp({
    email,
    options: {
      emailRedirectTo: callbackUrl,
    },
  });

  if (error) {
    return { error: error.message };
  }

  return { success: true };
}
What happens:
  • Supabase sends email with magic link
  • Email contains link like: https://yourapp.com/auth/callback?token_hash=abc123&type=email
  • User clicks link in email
3

Callback handles magic link

User clicks magic link → browser opens /auth/callback?token_hash=...&type=email
src/app/auth/callback/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const token_hash = searchParams.get("token_hash");
  const type = searchParams.get("type") as EmailOtpType | null;

  const supabase = await createClient();

  // Email/Magic Link flow
  if (token_hash && type) {
    const { error } = await supabase.auth.verifyOtp({
      type,
      token_hash,
    });

    if (!error) {
      // Redirect to dashboard after email verification
      if (type === "signup" || type === "email") {
        return NextResponse.redirect(`${origin}/dashboard`);
      }
    }
  }

  // On failure: redirect to sign-in with error
  return NextResponse.redirect(`${origin}/sign-in?error=auth_failed`);
}
What happens:
  • Supabase verifies the OTP token
  • If valid: creates session and redirects to /dashboard
  • If invalid/expired: redirects to /sign-in?error=auth_failed
Magic Links are ideal for:
  • Mobile-first apps (no password typing on small screens)
  • Security-conscious users (no password to remember/steal)
  • Quick sign-in from email (one-click login)
Magic Links expire after a certain time (default: 1 hour). If users click an expired link, they’ll see an error and must request a new one.Configure expiration in Supabase Dashboard → Authentication → Settings → Email → “Magic Link expiration”.

OAuth Flow

Sabo supports OAuth social login with Google, GitHub, and Apple.
1

Configure OAuth providers (Supabase)

  1. Go to Supabase Dashboard → Authentication → Providers
  2. Enable your desired providers (Google, GitHub, Apple)
  3. For each provider, set:
    • Client ID (from provider’s developer console)
    • Client Secret (from provider’s developer console)
    • Redirect URL: Use Supabase’s callback URL (automatically provided)
Provider setup guides:
2

User clicks OAuth button

From /sign-in or /sign-up pages:
src/components/auth/oauth-buttons.tsx
"use client";

export function OAuthButtons({ onOAuthSignIn }) {
  return (
    <>
      <Button onClick={() => onOAuthSignIn("google")}>
        <GoogleIcon /> Continue with Google
      </Button>
      <Button onClick={() => onOAuthSignIn("github")}>
        <GitHubIcon /> Continue with GitHub
      </Button>
      <Button onClick={() => onOAuthSignIn("apple")}>
        <AppleIcon /> Continue with Apple
      </Button>
    </>
  );
}
3

Server action initiates OAuth

src/app/(auth)/actions.ts
export async function signInWithOAuth(
  provider: "google" | "github" | "apple",
  redirectTo?: string | null
) {
  const supabase = await createClient();

  const callbackUrl = redirectTo
    ? `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback?next=${encodeURIComponent(redirectTo)}`
    : `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`;

  const { data, error } = await supabase.auth.signInWithOAuth({
    provider,
    options: {
      redirectTo: callbackUrl,
    },
  });

  if (error) {
    return { error: error.message };
  }

  if (data.url) {
    redirect(data.url);
  }
}
What happens:
  • Supabase generates OAuth URL for the provider
  • User is redirected to provider’s login page (e.g., Google sign-in)
  • User grants permission to your app
4

Provider redirects to callback

After user grants permission, provider redirects to:
https://yourapp.com/auth/callback?code=abc123&type=signup
src/app/auth/callback/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get("code");
  const type = searchParams.get("type");

  const supabase = await createClient();

  // OAuth flow
  if (code) {
    const { error } = await supabase.auth.exchangeCodeForSession(code);

    if (!error) {
      // Redirect to dashboard after OAuth signup
      if (type === "signup") {
        return NextResponse.redirect(`${origin}/dashboard`);
      }
      
      // Or redirect to custom location
      return NextResponse.redirect(`${origin}${next}`);
    }
  }

  return NextResponse.redirect(`${origin}/sign-in?error=auth_failed`);
}
What happens:
  • Callback exchanges OAuth code for session
  • Creates user account if first-time sign-in
  • Redirects to /dashboard (or custom next URL)
OAuth sign-in automatically creates a user account if it doesn’t exist. No separate sign-up flow needed.User data (name, email, avatar) is fetched from the OAuth provider and stored in Supabase.

Password Reset Flow

Users who forget their password can reset it via email.
1

User requests password reset (/forgot-password)

src/app/(auth)/forgot-password/page.tsx
"use client";

import { sendResetEmail } from "../actions";

export default function ForgotPasswordPage() {
  const [email, setEmail] = useState("");
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    const result = await sendResetEmail(email);
    
    if (result?.error) {
      setError(result.error);
    } else {
      // Redirect to sign-in with success message
      router.push("/sign-in?reset=true");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <Input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email address"
        required
      />
      <Button type="submit">Send reset link</Button>
    </form>
  );
}
2

Server action sends reset email

src/app/(auth)/actions.ts
export async function sendResetEmail(email: string) {
  const supabase = await createClient();

  const { error } = await supabase.auth.resetPasswordForEmail(email, {
    redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
  });

  if (error) {
    return { error: error.message };
  }

  return { success: true };
}
What happens:
  • Supabase sends password reset email
  • Email contains link like: https://yourapp.com/auth/callback?token_hash=xyz&type=recovery
  • User clicks link
3

Callback redirects to reset page

User clicks reset link → /auth/callback?token_hash=...&type=recovery
src/app/auth/callback/route.ts
if (token_hash && type) {
  const { error } = await supabase.auth.verifyOtp({
    type,
    token_hash,
  });

  if (!error) {
    // Redirect to reset-password page if recovery type
    if (type === "recovery") {
      return NextResponse.redirect(`${origin}/reset-password`);
    }
  }
}
What happens:
  • Callback verifies reset token
  • If valid: redirects to /reset-password
  • User is now authenticated with a temporary session
4

User enters new password (/reset-password)

src/app/(auth)/reset-password/page.tsx
"use client";

import { resetPassword } from "../actions";

export default function ResetPasswordPage() {
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (password.length < 8) {
      setError("Password must be at least 8 characters long.");
      return;
    }
    
    if (password !== confirmPassword) {
      setError("Passwords don't match.");
      return;
    }
    
    const result = await resetPassword(password);
    
    if (result?.error) {
      setError(result.error);
    }
    // Success: redirects to /sign-in
  };

  return (
    <form onSubmit={handleSubmit}>
      <Input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="New password"
        required
      />
      <Input
        type="password"
        value={confirmPassword}
        onChange={(e) => setConfirmPassword(e.target.value)}
        placeholder="Confirm password"
        required
      />
      <Button type="submit">Reset password</Button>
    </form>
  );
}
5

Server action updates password

src/app/(auth)/actions.ts
export async function resetPassword(newPassword: string) {
  const supabase = await createClient();

  const { error } = await supabase.auth.updateUser({
    password: newPassword,
  });

  if (error) {
    return { error: error.message };
  }

  revalidatePath("/", "layout");
  redirect("/sign-in");
}
What happens:
  • Supabase updates user’s password
  • Session is maintained (user is still authenticated)
  • Redirects to /sign-in with success message
  • User can now sign in with new password
Password reset links expire after 1 hour by default. If a user clicks an expired link, they’ll need to request a new one.Configure expiration in Supabase Dashboard → Authentication → Settings → Email → “Password recovery expiration”.
Profile fields (name, avatar, notification preferences) live in Supabase tables. Review Database with Supabase to see how auth events populate user_profiles and related tables.

Auth Callback Route

The /auth/callback route is the central hub for all authentication flows. It handles:
  • OAuth code exchange
  • Magic link verification
  • Email confirmation
  • Password reset links
src/app/auth/callback/route.ts
import { type EmailOtpType } from "@supabase/supabase-js";
import { NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get("code");
  const token_hash = searchParams.get("token_hash");
  const type = searchParams.get("type") as EmailOtpType | null;

  // Get "next" redirect parameter (default: /dashboard)
  let next = searchParams.get("next") ?? "/dashboard";
  
  // Only allow relative paths for security
  if (!next.startsWith("/")) {
    next = "/dashboard";
  }

  const supabase = await createClient();

  // OAuth flow (when code parameter exists)
  if (code) {
    const { error } = await supabase.auth.exchangeCodeForSession(code);

    if (!error) {
      // Redirect to reset-password page if recovery type
      if (type === "recovery") {
        return NextResponse.redirect(`${origin}/reset-password`);
      }

      // Redirect to dashboard after OAuth signup
      if (type === "signup") {
        return NextResponse.redirect(`${origin}/dashboard`);
      }

      // By default, redirect to next parameter or dashboard
      return NextResponse.redirect(`${origin}${next}`);
    }
  }

  // Email/Magic Link flow (when token_hash parameter exists)
  if (token_hash && type) {
    const { error } = await supabase.auth.verifyOtp({
      type,
      token_hash,
    });

    if (!error) {
      // Redirect to reset-password page if recovery type
      if (type === "recovery") {
        return NextResponse.redirect(`${origin}/reset-password`);
      }

      // Redirect to dashboard after email verification for signup
      if (type === "signup" || type === "email") {
        return NextResponse.redirect(`${origin}/dashboard`);
      }

      // For other types, use next parameter
      return NextResponse.redirect(`${origin}${next}`);
    }
  }

  // Redirect to sign-in page with error parameter on failure
  return NextResponse.redirect(`${origin}/sign-in?error=auth_failed`);
}
The next parameter allows you to redirect users to a specific page after authentication. For example:
/sign-in → Save "next=/pricing" to sessionStorage
→ OAuth flow → /auth/callback?code=...&next=/pricing
→ Redirects to /pricing after sign-in
This is useful for:
  • Deep linking (e.g., “Sign in to view this post”)
  • Checkout flows (e.g., “Sign in to subscribe”)
  • Protected resources (e.g., “Sign in to download”)
The next parameter only accepts relative paths (starting with /) to prevent open redirect vulnerabilities.External URLs are automatically sanitized to /dashboard.
For request/response schemas, status codes, and example tests for this route, see the Auth Callback API reference.

Error Handling & UI Patterns

Loading States

All auth actions should show loading indicators:
const [isLoading, setIsLoading] = useState(false);

const handleSubmit = async () => {
  setIsLoading(true);
  try {
    const result = await signIn(formData);
    // Handle result
  } finally {
    setIsLoading(false);
  }
};

<Button disabled={isLoading}>
  {isLoading ? (
    <>
      <Spinner />
      Signing in...
    </>
  ) : (
    "Sign in"
  )}
</Button>

Error States

Display errors from server actions:
const [error, setError] = useState<string | null>(null);

const handleSubmit = async () => {
  setError(null);
  const result = await signIn(formData);
  
  if (result?.error) {
    setError(result.error);
  }
};

{error && (
  <div className="text-sm text-red-500">
    {error}
  </div>
)}

Toast Notifications

Use sonner for success messages:
import { toast } from "sonner";

const handleMagicLinkSend = async () => {
  const result = await signInWithMagicLink(email);
  
  if (result?.error) {
    setError(result.error);
  } else {
    toast.success("Magic link sent!", {
      description: "Check your email to sign in."
    });
  }
};

Form Validation

Client-side validation before calling server actions:
const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  
  // Email validation
  if (!email.includes("@")) {
    setError("Please enter a valid email address.");
    return;
  }
  
  // Password validation
  if (password.length < 8) {
    setError("Password must be at least 8 characters long.");
    return;
  }
  
  // Password match validation
  if (password !== confirmPassword) {
    setError("Passwords don't match.");
    return;
  }
  
  // Clear error and proceed
  setError(null);
  const result = await signUp(formData);
  // ...
};

Query Parameter Banners

Show contextual messages based on URL parameters:
src/app/(auth)/sign-in/page.tsx
const searchParams = useSearchParams();
const resetParam = searchParams.get("reset");
const signupParam = searchParams.get("signup");
const errorParam = searchParams.get("error");

{resetParam === "true" && (
  <Alert>
    <AlertTitle>Password reset email sent!</AlertTitle>
    <AlertDescription>
      Please check your email and follow the instructions to reset your password.
    </AlertDescription>
  </Alert>
)}

{signupParam === "success" && (
  <Alert>
    <AlertTitle>Account created successfully!</AlertTitle>
    <AlertDescription>
      Please check your email and activate your account before signing in.
    </AlertDescription>
  </Alert>
)}

{errorParam === "auth_failed" && (
  <Alert variant="destructive">
    <AlertTitle>Authentication failed!</AlertTitle>
    <AlertDescription>
      Please try again.
    </AlertDescription>
  </Alert>
)}

Auth Routes Summary

RoutePurposeRedirects to
/sign-inEmail entry (step 1)/sign-in/confirm
/sign-in/confirmPassword or Magic Link (step 2)/dashboard (success)
/sign-upName + email entry (step 1)/sign-up/confirm
/sign-up/confirmPassword or Magic Link (step 2)/sign-in?signup=success
/forgot-passwordRequest password reset email/sign-in?reset=true
/reset-passwordEnter new password (after email link)/sign-in (success)
/auth/callbackOAuth & email verification handler/dashboard or /reset-password