> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getsabo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Auth with Supabase

> Add secure, session-based user authentication to your app built with Sabo using Supabase.

<Info>
  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.
</Info>

## Quick Setup

<Steps>
  <Step title="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](https://supabase.com/dashboard).
    * Set your Site URL and add `/auth/callback` to Additional Redirect URLs for dev and prod (Dashboard → Auth → URL Configuration). Guide: [Redirect URLs](https://supabase.com/docs/guides/auth/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](https://supabase.com/docs/guides/auth/social-login). Example guide: [Login with Google](https://supabase.com/docs/guides/auth/social-login/auth-google).

    <Tip>
      By default, Sabo uses a single `/auth/callback` to handle signup confirmation, password recovery, and OAuth code exchange in one place.
    </Tip>

    <Note>
      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](https://supabase.com/docs/guides/auth/redirect-urls).
    </Note>

    <Check>
      Confirm: env keys are set, `/auth/callback` is whitelisted for the domains you use, and (if enabled) providers redirect back successfully.
    </Check>
  </Step>

  <Step title="Define environment variables">
    Add these environment variables to your local `.env` (or `.env.local`) and to your hosting provider.

    ```bash .env theme={null}
    NEXT_PUBLIC_SUPABASE_URL=
    NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
    SUPABASE_SECRET_KEY=
    NEXT_PUBLIC_SITE_URL=http://localhost:3000
    ```

    <Warning>
      `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.
    </Warning>

    <Check>
      Run the app and ensure env vars are loaded (no undefined errors).
    </Check>
  </Step>

  <Step title="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.

    <Check>
      Run locally and ensure there are no missing env errors for `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.
    </Check>
  </Step>

  <Step title="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.

    <Info>
      For full patterns (matchers, includes/excludes, Stripe webhook exclusion, roles/tenants), see <a href="/core-features/routing-and-middleware">Routing & Middleware</a>.
    </Info>
  </Step>
</Steps>

***

## 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:

<Steps>
  <Step title="Step 1: Enter email (/sign-in)">
    User enters their email address on `/sign-in` page.

    ```tsx src/app/(auth)/sign-in/page.tsx theme={null}
    "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`
  </Step>

  <Step title="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

    ```tsx src/app/(auth)/sign-in/confirm/page.tsx theme={null}
    "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
  </Step>

  <Step title="Server action: signIn()">
    ```ts src/app/(auth)/actions.ts theme={null}
    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
  </Step>
</Steps>

<Tip>
  The two-step UX allows users to choose their preferred authentication method while keeping the UI clean and focused.
</Tip>

<Info>
  Need to customize confirmation, magic link, or password reset emails? Follow <a href="/core-features/auth-emails">Auth Emails with Resend</a> for template editing, SMTP setup, and deliverability guidance.
</Info>

### Sign Up Flow

Sign-up follows a similar two-step pattern:

<Steps>
  <Step title="Step 1: Enter name and email (/sign-up)">
    User enters full name and email address.

    ```tsx src/app/(auth)/sign-up/page.tsx theme={null}
    "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>
      );
    }
    ```
  </Step>

  <Step title="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

    ```tsx src/app/(auth)/sign-up/confirm/page.tsx theme={null}
    "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>
        </>
      );
    }
    ```
  </Step>

  <Step title="Server action: signUp()">
    ```ts src/app/(auth)/actions.ts theme={null}
    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`
  </Step>
</Steps>

<Warning>
  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".
</Warning>

***

## Magic Link Flow

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

<Steps>
  <Step title="User requests magic link">
    From either `/sign-in/confirm` or `/sign-up/confirm`, user clicks "Send magic link" button.

    ```tsx theme={null}
    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."
        });
      }
    };
    ```
  </Step>

  <Step title="Server action sends email">
    ```ts src/app/(auth)/actions.ts theme={null}
    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
  </Step>

  <Step title="Callback handles magic link">
    User clicks magic link → browser opens `/auth/callback?token_hash=...&type=email`

    ```ts src/app/auth/callback/route.ts theme={null}
    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`
  </Step>
</Steps>

<Tip>
  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)
</Tip>

<Warning>
  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".
</Warning>

***

## OAuth Flow

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

<Steps>
  <Step title="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)

    <Tip>
      Provider setup guides:

      * [Google OAuth](https://supabase.com/docs/guides/auth/social-login/auth-google)
      * [GitHub OAuth](https://supabase.com/docs/guides/auth/social-login/auth-github)
      * [Apple OAuth](https://supabase.com/docs/guides/auth/social-login/auth-apple)
    </Tip>
  </Step>

  <Step title="User clicks OAuth button">
    From `/sign-in` or `/sign-up` pages:

    ```tsx src/components/auth/oauth-buttons.tsx theme={null}
    "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>
        </>
      );
    }
    ```
  </Step>

  <Step title="Server action initiates OAuth">
    ```ts src/app/(auth)/actions.ts theme={null}
    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
  </Step>

  <Step title="Provider redirects to callback">
    After user grants permission, provider redirects to:

    ```
    https://yourapp.com/auth/callback?code=abc123&type=signup
    ```

    ```ts src/app/auth/callback/route.ts theme={null}
    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)
  </Step>
</Steps>

<Note>
  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.
</Note>

***

## Password Reset Flow

Users who forget their password can reset it via email.

<Steps>
  <Step title="User requests password reset (/forgot-password)">
    ```tsx src/app/(auth)/forgot-password/page.tsx theme={null}
    "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>
      );
    }
    ```
  </Step>

  <Step title="Server action sends reset email">
    ```ts src/app/(auth)/actions.ts theme={null}
    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
  </Step>

  <Step title="Callback redirects to reset page">
    User clicks reset link → `/auth/callback?token_hash=...&type=recovery`

    ```ts src/app/auth/callback/route.ts theme={null}
    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
  </Step>

  <Step title="User enters new password (/reset-password)">
    ```tsx src/app/(auth)/reset-password/page.tsx theme={null}
    "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>
      );
    }
    ```
  </Step>

  <Step title="Server action updates password">
    ```ts src/app/(auth)/actions.ts theme={null}
    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
  </Step>
</Steps>

<Warning>
  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".
</Warning>

<Tip>
  Profile fields (name, avatar, notification preferences) live in Supabase tables. Review <a href="/core-features/database-supabase">Database with Supabase</a> to see how auth events populate `user_profiles` and related tables.
</Tip>

***

## 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

```ts src/app/auth/callback/route.ts theme={null}
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`);
}
```

<Tip>
  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")
</Tip>

<Warning>
  The `next` parameter **only accepts relative paths** (starting with `/`) to prevent open redirect vulnerabilities.

  External URLs are automatically sanitized to `/dashboard`.
</Warning>

<Info>
  For request/response schemas, status codes, and example tests for this route, see the <a href="/api-reference/endpoint/auth-callback">Auth Callback API reference</a>.
</Info>

***

## Error Handling & UI Patterns

### Loading States

All auth actions should show loading indicators:

```tsx theme={null}
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:

```tsx theme={null}
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:

```tsx theme={null}
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:

```tsx theme={null}
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:

```tsx src/app/(auth)/sign-in/page.tsx theme={null}
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

| Route              | Purpose                               | Redirects to                      |
| ------------------ | ------------------------------------- | --------------------------------- |
| `/sign-in`         | Email entry (step 1)                  | `/sign-in/confirm`                |
| `/sign-in/confirm` | Password or Magic Link (step 2)       | `/dashboard` (success)            |
| `/sign-up`         | Name + email entry (step 1)           | `/sign-up/confirm`                |
| `/sign-up/confirm` | Password or Magic Link (step 2)       | `/sign-in?signup=success`         |
| `/forgot-password` | Request password reset email          | `/sign-in?reset=true`             |
| `/reset-password`  | Enter new password (after email link) | `/sign-in` (success)              |
| `/auth/callback`   | OAuth & email verification handler    | `/dashboard` or `/reset-password` |
