Skip to main content

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.

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.

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

Sign Up

Basic Email/Password Sign Up

'use server';

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

export async function signUp(formData: FormData) {
  const supabase = 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

'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

'use server';

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

export async function signIn(formData: FormData) {
  const supabase = 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

'use server';

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

export async function signInWithOAuth(provider: 'google' | 'github') {
  const supabase = 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:
'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. Do not default to bespoke HMAC-signed state blobs when the server only needs to confirm that the callback matches the browser session that initiated the flow.

Web QR Sign In

The platform web login page can create short-lived QR challenges for mobile approval:
  1. The browser first completes Cloudflare Turnstile. The QR card must not create or display a challenge until the client has a Turnstile token, and POST /api/v1/auth/qr-login/challenges verifies that token server-side before inserting a qr_login_challenges row with a hashed secret, request metadata, and a two-minute expiry.
  2. The browser 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 and challenge secret, then marks the challenge approved.
  5. The browser 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 only to validate the mobile approver.

Sign Out

'use server';

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

export async function signOut() {
  const supabase = 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.
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

'use server';

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

export async function enableMFA() {
  const supabase = 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

'use server';

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

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

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

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

MFA Challenge During Sign In

'use server';

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

export async function signInWithMFA(email: string, password: string) {
  const supabase = 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

'use server';

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

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

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

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

Session Management

Get Current Session

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

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

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

  if (error) throw error;
  return session;
}

Get Current User

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

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

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

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

  return user;
}

Refresh Session

'use server';

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

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

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

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

Password Reset

Request Password Reset

'use server';

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

export async function requestPasswordReset(email: string) {
  const supabase = 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

'use server';

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

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

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

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

Email Verification

Resend Verification Email

'use server';

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

export async function resendVerificationEmail() {
  const supabase = 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 with createPOST('<app>') so /verify-token can exchange the cross-app token for a host-only tuturuuu_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.
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

import { generateCrossAppToken } from '@tuturuuu/auth/cross-app';

export async function createShortenerLink(userId: string) {
  // Generate token that shortener app can verify
  const token = await generateCrossAppToken(userId, {
    expiresIn: '1h',
    app: 'shortener',
  });

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

Verify Cross-App Token

import { verifyCrossAppToken } from '@tuturuuu/auth/cross-app';

export async function handleCrossAppRequest(token: string) {
  const payload = await verifyCrossAppToken(token);

  if (!payload) {
    throw new Error('Invalid token');
  }

  // payload contains: { userId, app, exp, iat }
  return payload;
}

Middleware Authentication

Protect routes using Next.js middleware:
// middleware.ts
import { createDynamicClient } from '@tuturuuu/supabase/next/server';
import { NextRequest, NextResponse } from 'next/server';

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

  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'],
};

Protected Server Component

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

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

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

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

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

Client-Side Authentication

useUser Hook

'use client';

import { createClient } from '@tuturuuu/supabase/next/client';
import { useEffect, useState } from 'react';
import type { SupabaseUser } from '@tuturuuu/supabase/next/user';

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

  useEffect(() => {
    // Get initial session
    supabase.auth.getSession().then(({ data: { session } }) => {
      setUser(session?.user ?? null);
      setLoading(false);
    });

    // Listen for auth changes
    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((_event, session) => {
      setUser(session?.user ?? null);
    });

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

  return { user, loading };
}

Usage

'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:
'use server';

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

export async function linkIdentity(provider: 'google' | 'github') {
  const supabase = 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
    const { data: { user } } = await supabase.auth.getUser();
    if (!user) throw new Error('Unauthorized');
    
  2. Redirect after authentication
    redirect('/dashboard'); // Don't return sensitive data
    
  3. Handle errors gracefully
    if (error) return { error: error.message };
    
  4. Implement MFA for sensitive operations
    const mfaRequired = await checkMFAEnabled(userId);
    

❌ DON’T

  1. Don’t trust client-side auth state alone
    // ❌ Bad
    if (localStorage.getItem('user')) { /* ... */ }
    
  2. Don’t expose sensitive data in auth redirects
    // ❌ Bad
    redirect(`/dashboard?apiKey=${apiKey}`);
    
  3. Don’t store passwords
    // ❌ Bad: Let Supabase handle password storage
    
  4. Don’t use createAdminClient() for auth operations
    // ❌ Bad: Use regular client
    const sbAdmin = await createAdminClient();
    await sbAdmin.auth.signUp({ ... });
    

External Resources