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) │
- User signs in via Supabase Auth
- Supabase validates credentials against database
- Client receives session token
- 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:
- 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.
- The browser renders a
tuturuuu://auth/qr-login payload that contains the
challenge id, one-time secret, and web origin.
- The signed-in mobile app scans the code from Settings > Session. Mobile must
have app lock enabled, then performs local authentication before approving.
POST /api/v1/auth/qr-login/challenges/:id/approve validates the mobile
Bearer session and challenge secret, then marks the challenge approved.
- 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:
- Call
supabase.auth.getClaims() first.
- Use
claims.sub as the authenticated user id when available.
- 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
-
Always check authentication server-side
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Unauthorized');
-
Redirect after authentication
redirect('/dashboard'); // Don't return sensitive data
-
Handle errors gracefully
if (error) return { error: error.message };
-
Implement MFA for sensitive operations
const mfaRequired = await checkMFAEnabled(userId);
❌ DON’T
-
Don’t trust client-side auth state alone
// ❌ Bad
if (localStorage.getItem('user')) { /* ... */ }
-
Don’t expose sensitive data in auth redirects
// ❌ Bad
redirect(`/dashboard?apiKey=${apiKey}`);
-
Don’t store passwords
// ❌ Bad: Let Supabase handle password storage
-
Don’t use
createAdminClient() for auth operations
// ❌ Bad: Use regular client
const sbAdmin = await createAdminClient();
await sbAdmin.auth.signUp({ ... });
External Resources