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

# Authentication Patterns

> Authentication implementation patterns in the Tuturuuu platform

The Tuturuuu platform uses Supabase Auth for user authentication with support for email/password, OAuth providers, multi-factor authentication (MFA), and cross-app token authentication.

<Note>
  The server helpers in `@tuturuuu/supabase/next/server` resolve cookies (and an
  optional `request` argument) asynchronously, so `createClient()` and
  `createDynamicClient()` are **async** and must be awaited. `createAdminClient()`
  is synchronous, but its return type allows awaiting, so existing call sites that
  `await createAdminClient()` still work — never `await createClient()` results
  without the keyword.
</Note>

## Authentication Flow

```
┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│   Client     │──1──>│  Supabase    │──2──>│  Database    │
│ (Browser/App)│      │  Auth        │      │  (RLS)       │
└──────────────┘      └──────────────┘      └──────────────┘
       │                      │                      │
       │<─────────3───────────│                      │
       │  Session Token       │                      │
       │                      │                      │
       │──────4──────────────────────────────────────>│
       │  Authenticated Request (w/ session)          │
       │<─────────────────────────────────────────────│
       │  Data (filtered by RLS)                      │
```

1. User signs in via Supabase Auth
2. Supabase validates credentials against database
3. Client receives session token
4. Subsequent requests include session token for RLS

## Cross-App Supabase Cookies

Production Tuturuuu browser sessions use one canonical Supabase auth cookie for
the configured Supabase project and share it across `*.tuturuuu.com` by setting
`Domain=.tuturuuu.com`. Portless local development does the same across
`*.tuturuuu.localhost` with `Domain=.tuturuuu.localhost`.

Keep the cookie host-only for plain `localhost`, preview deployments, and
unrelated domains. Registered satellite apps still keep Tuturuuu app-session
JWTs as a fallback and API isolation mechanism, so do not remove app-session
refresh or handoff paths when updating shared Supabase cookie behavior.

## Web Multi-Account Sessions

`apps/web` stores multi-account sessions in a server-owned vault instead of
browser `localStorage`. The browser keeps only an HttpOnly device cookie and
loads account summaries from `/api/v1/auth/accounts`; Supabase access and
refresh tokens remain encrypted in `private.web_account_sessions`.

Use `WEB_MULTI_ACCOUNT_SESSION_SECRET` for vault encryption when available. If
it is not configured, the server falls back to `SUPABASE_SECRET_KEY`,
`SUPABASE_SERVICE_ROLE_KEY`, then `SUPABASE_SERVICE_KEY`. Never expose those
values to client components or upload legacy browser-stored sessions into the
vault; users should re-add accounts after storage migrations.

## Sign Up

### Basic Email/Password Sign Up

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";
import { redirect } from "next/navigation";

export async function signUp(formData: FormData) {
  const supabase = await createClient();

  const email = formData.get("email") as string;
  const password = formData.get("password") as string;
  const displayName = formData.get("displayName") as string;

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

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

  redirect("/verify-email");
}
```

### Client Component

```tsx theme={null}
"use client";

import { signUp } from "./actions";
import { useState } from "react";

export function SignUpForm() {
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(formData: FormData) {
    const result = await signUp(formData);
    if (result?.error) {
      setError(result.error);
    }
  }

  return (
    <form action={handleSubmit}>
      <input name="displayName" placeholder="Display Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <input name="password" type="password" placeholder="Password" required />
      {error && <p className="text-dynamic-red">{error}</p>}
      <button type="submit">Sign Up</button>
    </form>
  );
}
```

## Sign In

### Email/Password Sign In

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";
import { redirect } from "next/navigation";

export async function signIn(formData: FormData) {
  const supabase = await createClient();

  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

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

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

  redirect("/dashboard");
}
```

### OAuth Sign In

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function signInWithOAuth(provider: "google" | "github") {
  const supabase = await createClient();

  const { data, error } = await supabase.auth.signInWithOAuth({
    provider,
    options: {
      redirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`,
    },
  });

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

  return { url: data.url };
}
```

Client Component:

```tsx theme={null}
"use client";

import { signInWithOAuth } from "./actions";

export function OAuthButtons() {
  async function handleOAuthSignIn(provider: "google" | "github") {
    const result = await signInWithOAuth(provider);
    if (result.url) {
      window.location.href = result.url;
    }
  }

  return (
    <div>
      <button onClick={() => handleOAuthSignIn("google")}>
        Sign in with Google
      </button>
      <button onClick={() => handleOAuthSignIn("github")}>
        Sign in with GitHub
      </button>
    </div>
  );
}
```

### OAuth Callback State

For provider-specific OAuth callbacks that need to carry ephemeral verifier or
CSRF state across a browser redirect, prefer a short-lived HttpOnly cookie scoped
to the callback route. When the callback binds external credentials or mutates a
workspace, do not let the cookie value be the sole authority: use a signed,
expiry-bound state payload that also binds the workspace or account being
connected, then require the callback cookie and query `state` to match before
verifying that payload.

### Email-Based Auth Recovery

Infrastructure admins can use **Infrastructure > Auth Recovery** when a
manually reviewed user cannot complete normal OTP or password sign-in because of
email-scoped infrastructure blocks, stale Supabase bans, or OTP counters.

The support flow is:

1. Search the email on the Auth Recovery page and inspect diagnostics.
2. Create a recovery override with a support reason. Overrides default to 7 days.
3. Keep both **normal login** and **recovery email** enabled unless the case
   needs only one path.
4. Use **Send recovery email**. The platform sends the email; admins should not
   copy tokens or links manually.
5. The user can click the recovery link or enter the 6-digit code on
   `/auth/recovery`. Recovery credentials expire after 15 minutes and are
   single-use.
6. Revoke the override once the user has recovered access or the case no longer
   needs support access.

Normal-login overrides bypass only email-scoped infrastructure and OTP/password
rate-limit blocks. They still enforce malformed request validation, suspicious
user-agent checks, Turnstile, password correctness, MFA, and active IP blocks
unless an admin separately clears those IP blocks. When a valid override is used,
the service also attempts to clear the Supabase auth ban and confirm the email.

Recovery-email sign-in stores token and code hashes in private schema tables and
audits sends, consumes, rejects, creates, revokes, and Supabase unban/create
attempts. The recovery session is created with the existing admin
`generateLink` plus detached `verifyOtp` pattern, then normal auth cookies are
set. Redirects are limited to sanitized app paths.

Database objects live in:

* `private.auth_recovery_overrides`
* `private.auth_recovery_tokens`
* `private.auth_recovery_events`

Use `AUTH_RECOVERY_HASH_SECRET` in production when available. The code falls
back to existing Supabase server secrets for local development, but do not expose
those values to clients or support tooling. Do not run `bun sb:push` for this
change; apply migrations through the normal database release process.

### Passkeys

Passkeys use Supabase Auth's experimental WebAuthn APIs. Browser clients created
through `@tuturuuu/supabase/next/auth-browser` must pass
`auth.experimental.passkey: true`; otherwise Supabase rejects
`auth.signInWithPasskey()`, `auth.registerPasskey()`, and
`auth.passkey.*` calls.

`apps/web` owns passkey UX:

1. The public login form exposes an explicit "Continue with passkey" action so
   users can open the browser passkey picker without relying only on autofill.
2. Account Security in the apps/web settings dialog owns passkey registration,
   rename, and delete actions. Do not add separate passkey settings pages.
3. Satellite apps should continue to route user authentication through apps/web
   cross-app auth. Passkeys are bound to the relying party domain, so the central
   apps/web origin remains the authority for Tuturuuu account passkeys.

Production Supabase Auth must have passkeys enabled with these relying party
settings:

* Relying Party Display Name: `Tuturuuu`
* Relying Party ID: `tuturuuu.com`
* Relying Party Origins: `https://tuturuuu.com`

Local Supabase Auth must also enable passkeys in
`apps/database/supabase/config.toml`. The committed local config uses the
Portless apps/web origin:

* Relying Party Display Name: `Tuturuuu`
* Relying Party ID: `tuturuuu.localhost`
* Relying Party Origins: `https://tuturuuu.localhost`

Restart local Supabase after changing this config; the Auth service reads these
settings on startup. Real WebAuthn ceremonies require a secure origin, so test
registration and sign-in from `https://tuturuuu.localhost`. UI-only and
unsupported-browser paths can still be exercised without registering a
credential.

Remote Supabase development auth is different: if a cloud Supabase project has
captcha protection enabled, passkey sign-in must send a real Turnstile token.
The local E2E bypass is honored only when `NEXT_PUBLIC_SUPABASE_URL` points at
local Supabase. When testing remote Supabase from `https://tuturuuu.localhost`,
the Turnstile site key must authorize that local hostname; Cloudflare Turnstile
error `110200` means the widget cannot mint the token Supabase requires. The
login UI keeps passkey sign-in blocked while that token is missing; either add
the local hostname to the Cloudflare Turnstile widget used by the Supabase
project, or point `NEXT_PUBLIC_SUPABASE_URL` at local Supabase for dev passkey
testing.

### Web QR Session Handoff

QR-based session handoff must never be a public login bootstrap. An
unauthenticated browser cannot prove that it belongs to the account scanning the
QR code, so public challenge creation would allow QR phishing where an attacker
polls a victim-approved challenge and receives the victim's session.

The QR challenge endpoints are therefore constrained to authenticated,
same-account handoff:

1. `POST /api/v1/auth/qr-login/challenges` validates the request origin and the
   request-scoped Supabase session before inserting a `qr_login_challenges` row.
   The row stores a hashed secret, request metadata, `creatorUserId`, and a
   two-minute expiry.
2. The client renders a `tuturuuu://auth/qr-login` payload that contains the
   challenge id, one-time secret, and web origin.
3. The signed-in mobile app scans the code from Settings > Session. Mobile must
   have app lock enabled, then performs local authentication before approving.
4. `POST /api/v1/auth/qr-login/challenges/:id/approve` validates the mobile
   Bearer session, challenge secret, and `creatorUserId`. The approver must be
   the same user that created the challenge.
5. The creator polls `GET /api/v1/auth/qr-login/challenges/:id?secret=...`.
   Once approved, the server consumes the challenge and creates a fresh detached
   Supabase session via admin `generateLink` plus detached `verifyOtp`.

QR challenge rows never store access or refresh tokens. The table is RLS-enabled
without anon/authenticated grants; API routes use the service role for challenge
state and the request-scoped client to validate both the challenge creator and
the mobile approver.

## Sign Out

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";
import { redirect } from "next/navigation";

export async function signOut() {
  const supabase = await createClient();
  await supabase.auth.signOut();
  redirect("/login");
}
```

## Server-side Auth Resolution (`getClaims` first)

For server-side route and helper authorization checks, prefer a claims-first flow:

1. Call `supabase.auth.getClaims()` first.
2. Use `claims.sub` as the authenticated user id when available.
3. Fall back to `supabase.auth.getUser()` when claims are unavailable or insufficient.

This reduces auth latency on hot API paths while preserving correctness for call sites
that still need canonical user resolution.

```typescript theme={null}
async function resolveCurrentUserId(supabase: TypedSupabaseClient) {
  const getClaims = (supabase.auth as { getClaims?: () => Promise<unknown> })
    .getClaims;

  if (typeof getClaims === "function") {
    const { data, error } = (await getClaims.call(supabase.auth)) as {
      data?: { claims?: { sub?: string } };
      error?: unknown;
    };

    if (!error && data?.claims?.sub) {
      return data.claims.sub;
    }
  }

  const {
    data: { user },
  } = await supabase.auth.getUser();

  return user?.id ?? null;
}
```

When implementing this pattern, feature-detect `getClaims` first. Some tests and
older stubs only mock `getUser`, and unconditional `getClaims` calls will break
those environments.

## Multi-Factor Authentication (MFA)

### Enable TOTP MFA

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function enableMFA() {
  const supabase = await createClient();

  // Enroll in MFA
  const { data, error } = await supabase.auth.mfa.enroll({
    factorType: "totp",
  });

  if (error) throw error;

  return {
    qrCode: data.totp.qr_code, // Display to user
    secret: data.totp.secret, // For manual entry
    factorId: data.id,
  };
}
```

### Verify MFA Enrollment

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function verifyMFAEnrollment(factorId: string, code: string) {
  const supabase = await createClient();

  const { data, error } = await supabase.auth.mfa.challengeAndVerify({
    factorId,
    code,
  });

  if (error) throw error;
  return { success: true };
}
```

### MFA Challenge During Sign In

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function signInWithMFA(email: string, password: string) {
  const supabase = await createClient();

  // Initial sign in
  const { data: signInData, error: signInError } =
    await supabase.auth.signInWithPassword({
      email,
      password,
    });

  if (signInError) throw signInError;

  // Check if MFA is required
  const { data: factors } = await supabase.auth.mfa.listFactors();

  if (factors && factors.totp.length > 0) {
    const factorId = factors.totp[0].id;

    // Create MFA challenge
    const { data: challengeData, error: challengeError } =
      await supabase.auth.mfa.challenge({ factorId });

    if (challengeError) throw challengeError;

    return {
      requiresMFA: true,
      challengeId: challengeData.id,
      factorId,
    };
  }

  return { requiresMFA: false };
}
```

### Verify MFA Code

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function verifyMFACode(
  factorId: string,
  challengeId: string,
  code: string,
) {
  const supabase = await createClient();

  const { data, error } = await supabase.auth.mfa.verify({
    factorId,
    challengeId,
    code,
  });

  if (error) throw error;
  return { success: true };
}
```

### Mobile MFA Approval Cookies

Mobile approval for web MFA is scoped to the Supabase login session that created
and consumed the approval challenge. When the web browser polls an approved
mobile MFA challenge, store the current JWT `session_id` in the challenge
approval metadata. The auth proxy must compare that stored session id with the
current request claims before using `ttr_mfa_mobile_approval` to bypass the MFA
redirect.

Do not treat the approval cookie as a user-scoped remember-me token. A valid
cookie only proves that one challenge secret was approved; it must also match
the current Supabase session. Central logout responses should expire the
approval cookie, and MFA redirects should clear stale approval cookies that do
not satisfy the current-session binding.

## Session Management

### Get Current Session

```typescript theme={null}
import { createClient } from "@tuturuuu/supabase/next/server";

export async function getSession() {
  const supabase = await createClient();

  const {
    data: { session },
    error,
  } = await supabase.auth.getSession();

  if (error) throw error;
  return session;
}
```

### Get Current User

```typescript theme={null}
import { createClient } from "@tuturuuu/supabase/next/server";

export async function getCurrentUser() {
  const supabase = await createClient();

  const {
    data: { user },
    error,
  } = await supabase.auth.getUser();

  if (error || !user) {
    return null;
  }

  return user;
}
```

### Refresh Session

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function refreshSession() {
  const supabase = await createClient();

  const { data, error } = await supabase.auth.refreshSession();

  if (error) throw error;
  return data.session;
}
```

## Password Reset

### Request Password Reset

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function requestPasswordReset(email: string) {
  const supabase = await createClient();

  const { error } = await supabase.auth.resetPasswordForEmail(email, {
    redirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/auth/reset-password`,
  });

  if (error) throw error;
  return { success: true };
}
```

### Reset Password

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function resetPassword(newPassword: string) {
  const supabase = await createClient();

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

  if (error) throw error;
  return { success: true };
}
```

## Email Verification

### Resend Verification Email

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function resendVerificationEmail() {
  const supabase = await createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user?.email) {
    throw new Error("No user email found");
  }

  const { error } = await supabase.auth.resend({
    type: "signup",
    email: user.email,
  });

  if (error) throw error;
  return { success: true };
}
```

## Cross-App Authentication

The platform supports token-based authentication across different apps using `@tuturuuu/auth/cross-app`.

When a new satellite app participates in centralized login, wire both sides in the same patch:

* Register the app URL in `packages/utils/src/internal-domains.ts` so `mapUrlToApp(...)` can recognize its `returnUrl`.
* Add `apps/<app>/src/app/api/auth/verify-app-token/route.ts` so
  `/verify-token` can exchange the cross-app token for a host-only
  `tuturuuu_app_session` cookie. When the satellite requires rewritten
  `apps/web` API access, use `createPOST('<app>', { verificationBaseUrl:
  WEB_APP_URL })` so the handoff also stores the Web-issued app-session cookie.
* Keep `generate_cross_app_token(...)` bound to the authenticated caller
  (`p_user_id = auth.uid()`) so verify endpoints cannot mint sessions for
  arbitrary users.
* Do not call `supabase.auth.setSession()` in registered internal apps. The
  verifier route sets the HttpOnly app-session cookie, and satellite UI should
  fetch user/profile data by forwarding that cookie to central internal APIs.
* Registered internal app source must not call `supabase.auth.*` directly.
  Use `@tuturuuu/auth/app-session` server helpers and `@tuturuuu/internal-api`
  profile/default-workspace helpers instead; `bun check` runs the static guard
  across all registered app `src` directories.
* App-session auth is read/update oriented for satellite apps. Destructive
  workspace operations such as `DELETE /api/workspaces/[wsId]` require a full
  Supabase session (cookie or bearer) and `manage_workspace_settings`; they do
  not opt into `allowAppSessionAuth` because the app-session path uses an
  admin-backed client that would bypass workspace delete RLS.

If either piece is missing, centralized login can stall on the web login spinner or land on `/verify-token` with no token-verification endpoint to finish the handoff.

### Generate Cross-App Token

`generateCrossAppToken(supabase, targetApp, originApp, expirySeconds?)` is the
real signature. It reads the authenticated user from the passed Supabase client,
calls the `generate_cross_app_token` RPC, and returns the token string (or
`null` on failure). The origin app (`'web'`) mints the token; the target app
(`'shortener'`, `'nova'`, `'rewise'`, etc.) verifies it.

```typescript theme={null}
import { generateCrossAppToken } from "@tuturuuu/auth/cross-app";
import { createClient } from "@tuturuuu/supabase/next/server";

export async function createShortenerLink() {
  const supabase = await createClient();

  // Generate a token the shortener app can verify (default expiry: 300s).
  const token = await generateCrossAppToken(
    supabase,
    "shortener", // targetApp
    "web", // originApp
    3600, // expirySeconds (optional)
  );

  if (!token) {
    throw new Error("Failed to generate cross-app token");
  }

  return {
    url: `${process.env.SHORTENER_APP_URL}/create?token=${token}`,
  };
}
```

### Validate Cross-App Token

The target app validates the token with
`validateCrossAppToken(supabase, token, targetApp)`, which calls the
`validate_cross_app_token_with_session` RPC and returns `{ userId }` (or `null`).
The target app is responsible for establishing its own session/app-session from
that `userId`; the token never carries access or refresh tokens.

```typescript theme={null}
import { validateCrossAppToken } from "@tuturuuu/auth/cross-app";
import { createClient } from "@tuturuuu/supabase/next/server";

export async function handleCrossAppRequest(token: string) {
  const supabase = await createClient();

  const result = await validateCrossAppToken(supabase, token, "shortener");

  if (!result) {
    throw new Error("Invalid token");
  }

  // result contains: { userId }
  return result;
}
```

<Note>
  `@tuturuuu/auth/cross-app` does **not** export a `verifyCrossAppToken`
  function. For the browser-side handoff on `/verify-token`, use the exported
  `verifyRouteToken({ searchParams, token, router })` helper, which POSTs the
  token to `/api/auth/verify-app-token` and lets the verifier route set the
  HttpOnly app-session cookie. `revokeAllCrossAppTokens(supabase)` invalidates a
  user's outstanding tokens.
</Note>

## Proxy (Edge Middleware) Authentication

In `apps/web` the edge entry point that protects routes lives in
`apps/web/src/proxy.ts` (not a `middleware.ts` file). It exports an async
`proxy(req)` function plus a `config.matcher`, and Next.js is configured to use
this file as the request middleware. The real implementation delegates the
heavy lifting to `createCentralizedAuthProxy` from `@tuturuuu/auth/proxy`, then
layers on onboarding checks, workspace-slug normalization, guest-route guards,
and locale handling. See [Routing](/platform/architecture/routing) for how the
proxy coordinates those concerns.

The simplified example below shows the core shape: resolve the user from a
request-scoped Supabase client and redirect when unauthenticated. Note that
`createDynamicClient()` is async and must be awaited.

```typescript theme={null}
// apps/web/src/proxy.ts (simplified)
import { createClient } from "@tuturuuu/supabase/next/server";
import { type NextRequest, NextResponse } from "next/server";

export async function proxy(request: NextRequest) {
  const supabase = await createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  // Protect dashboard routes
  if (request.nextUrl.pathname.startsWith("/dashboard") && !user) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  // Redirect authenticated users from auth pages
  if (request.nextUrl.pathname.startsWith("/login") && user) {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/login", "/signup"],
};
```

The production proxy resolves the user with
`resolveAuthenticatedSessionUser(supabase)` and propagates refreshed auth
cookies onto every redirect via `propagateAuthCookies(authRes, response)`.
Reuse those helpers instead of re-implementing session resolution and cookie
forwarding when you extend the proxy.

## Protected Server Component

```typescript theme={null}
import { createClient } from '@tuturuuu/supabase/next/server';
import { redirect } from 'next/navigation';

export default async function ProtectedPage() {
  const supabase = await createClient();

  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    redirect('/login');
  }

  return (
    <div>
      <h1>Welcome, {user.email}</h1>
    </div>
  );
}
```

## Client-Side Authentication

<Warning>
  `@tuturuuu/supabase/next/client` is **deprecated** for CRUD/storage and may
  throw unless a compatibility flag is set. For browser data access, use
  `@tuturuuu/internal-api`. For the narrow case where the browser genuinely
  needs an auth client (reacting to live auth-state changes), use
  `createAuthClient()` from `@tuturuuu/supabase/next/auth-browser`. Do not fetch
  product data on the client through raw Supabase clients, and prefer TanStack
  Query over `useEffect` for any data fetching.
</Warning>

### useUser Hook (auth-state subscription)

Subscribing to Supabase auth-state changes is a legitimate exception to the
"no `useEffect` for data fetching" guidance: this hook does not fetch product
data, it mirrors the live session into React state. Use `createAuthClient` and
subscribe once.

```tsx theme={null}
"use client";

import type { User } from "@supabase/supabase-js";
import { createAuthClient } from "@tuturuuu/supabase/next/auth-browser";
import { useEffect, useMemo, useState } from "react";

export function useUser() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const supabase = useMemo(() => createAuthClient(), []);

  useEffect(() => {
    // Resolve the current user once on mount.
    supabase.auth.getUser().then(({ data: { user } }) => {
      setUser(user ?? null);
      setLoading(false);
    });

    // Then keep it in sync with live auth-state changes.
    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((_event, session) => {
      setUser(session?.user ?? null);
    });

    return () => subscription.unsubscribe();
  }, [supabase]);

  return { user, loading };
}
```

<Note>
  For authorization decisions, always re-verify the user server-side
  (`await supabase.auth.getUser()` or the `getClaims`-first pattern above). Client
  auth state is for UX only — never trust it as the sole gate for protected data.
</Note>

### Usage

```tsx theme={null}
"use client";

import { useUser } from "@/hooks/useUser";

export function UserProfile() {
  const { user, loading } = useUser();

  if (loading) return <div>Loading...</div>;
  if (!user) return <div>Not authenticated</div>;

  return <div>Logged in as {user.email}</div>;
}
```

## Identity Linking

Link multiple auth providers to same account:

```typescript theme={null}
"use server";

import { createClient } from "@tuturuuu/supabase/next/server";

export async function linkIdentity(provider: "google" | "github") {
  const supabase = await createClient();

  const { data, error } = await supabase.auth.linkIdentity({
    provider,
  });

  if (error) throw error;
  return { url: data.url };
}
```

## Best Practices

### ✅ DO

1. **Always check authentication server-side**

   ```typescript theme={null}
   const {
     data: { user },
   } = await supabase.auth.getUser();
   if (!user) throw new Error("Unauthorized");
   ```

2. **Redirect after authentication**

   ```typescript theme={null}
   redirect("/dashboard"); // Don't return sensitive data
   ```

3. **Handle errors gracefully**

   ```typescript theme={null}
   if (error) return { error: error.message };
   ```

4. **Implement MFA for sensitive operations**
   ```typescript theme={null}
   const mfaRequired = await checkMFAEnabled(userId);
   ```

### ❌ DON'T

1. **Don't trust client-side auth state alone**

   ```typescript theme={null}
   // ❌ Bad
   if (localStorage.getItem("user")) {
     /* ... */
   }
   ```

2. **Don't expose sensitive data in auth redirects**

   ```typescript theme={null}
   // ❌ Bad
   redirect(`/dashboard?apiKey=${apiKey}`);
   ```

3. **Don't store passwords**

   ```typescript theme={null}
   // ❌ Bad: Let Supabase handle password storage
   ```

4. **Don't use `createAdminClient()` for auth operations**
   ```typescript theme={null}
   // ❌ Bad: Use regular client
   const sbAdmin = await createAdminClient();
   await sbAdmin.auth.signUp({ ... });
   ```

## Related Documentation

* [Authorization](/platform/architecture/authorization) - Permission system
* [Supabase Client](/reference/packages/supabase) - Client creation patterns
* [RLS Policies](/reference/database/rls-policies) - Database security

## External Resources

* [Supabase Auth Documentation](https://supabase.com/docs/guides/auth)
* [Next.js Authentication](https://nextjs.org/docs/app/building-your-application/authentication)
