Authentication
Configure sign-in methods, OAuth providers, and multi-factor authentication.
Overview
Authentication is handled by self-hosted Supabase Auth. It supports:
- Email/password sign-up and sign-in
- OAuth providers (Google, Microsoft, GitHub, Apple, Discord)
- Magic links: passwordless email authentication
- Multi-factor authentication (TOTP)
All auth state is managed through the useAuth() hook on the client and JWT middleware on the server.
useAuth() Hook
The useAuth() hook (from AuthProvider) exposes everything you need for auth in React components:
| Property |
Type |
Description |
user |
User | null |
Current Supabase user object |
session |
Session | null |
Current session with tokens |
isLoading |
boolean |
true while session is being validated |
isSuperAdmin |
boolean |
User has super_admin role in app_metadata |
mfaRequired |
boolean |
MFA is enforced app-wide (set by admin) |
hasMfaEnabled |
boolean |
Current user has enrolled a TOTP factor |
signIn |
object |
Sign-in methods (see below) |
signUp |
function |
Create a new account |
signOut |
function |
Sign out and clear session |
mfa |
object |
MFA operations (enroll, challenge, verify) |
impersonateUser |
function |
Impersonate a user (super admin only) |
stopImpersonating |
function |
Return to admin session |
import { useAuth } from '@/components/auth/AuthProvider';
function MyComponent() {
const { user, isLoading, isSuperAdmin } = useAuth();
if (isLoading) return <p>Loading...</p>;
if (!user) return <p>Not signed in</p>;
return <p>Hello, {user.email}</p>;
}
Sign-In Methods
Email & Password
The default sign-in method. Calls the server-side login endpoint which handles account lockout checks before authenticating.
const { signIn } = useAuth();
try {
const { requiresMfa } = await signIn.email('user@example.com', 'password');
if (requiresMfa) {
// Redirect to MFA verification page
}
} catch (error) {
// Handle error (invalid credentials, account locked, etc.)
}
The signIn.email() method returns { requiresMfa: boolean }. When true, the user has MFA enrolled and must verify a TOTP code before accessing the app.
Magic Links
Users receive a sign-in link via email; no password required. Requires SMTP to be configured.
const { signIn } = useAuth();
await signIn.magicLink('user@example.com');
// User receives an email with a sign-in link
OAuth Providers
Enable social login by configuring providers. Each provider has a dedicated method:
const { signIn } = useAuth();
await signIn.google();
await signIn.github();
await signIn.microsoft();
await signIn.apple();
await signIn.discord();
To enable an OAuth provider (self-hosted auth reads these from environment variables, and there is no Supabase Studio step):
- Set the provider's credentials as environment variables.
vibecarbon configure → OAuth does this for Google and Microsoft, writing GOOGLE_ENABLED / GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET (and the Microsoft equivalents) to .env / .env.local. For other providers, set them by hand. vibecarbon deploy ships them to the server, where GoTrue reads them as GOTRUE_EXTERNAL_*.
- In the provider's developer console, set the authorized redirect URI to
<SUPABASE_URL>/auth/v1/callback (the configure wizard prints the exact URL to paste).
- The provider button automatically appears on the sign-in page.
Sign-Up
Create a new account with email, password, and an optional display name:
const { signUp } = useAuth();
await signUp('user@example.com', 'password', 'Jane Smith');
// User is signed in and redirected to onboarding
The name parameter is stored in user_metadata.full_name.
Route Protection
Two route guard components are defined in App.tsx:
ProtectedRoute requires any authenticated user. Redirects to /sign-in if not logged in. Also redirects to /mfa-verify if MFA is required but not yet verified.
SuperAdminProtectedRoute requires the super_admin role. Redirects to the dashboard if the user is not a super admin.
// In App.tsx routes
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/admin/*"
element={
<SuperAdminProtectedRoute>
<AdminPanel />
</SuperAdminProtectedRoute>
}
/>
Public pages (like the landing page, blog, docs) don't need a wrapper.
Multi-Factor Authentication
Enrollment
Users enroll in MFA from Settings → Security. The flow:
- Call
mfa.enroll() to generate a TOTP secret and QR code
- User scans the QR code with an authenticator app
- User enters a TOTP code to verify enrollment
const { mfa } = useAuth();
// Step 1: Enroll
const data = await mfa.enroll('My Authenticator');
// data.totp.qr_code: data URI for the QR code
// data.totp.uri: otpauth:// URI for manual entry
// Step 2: Verify enrollment with a code from the authenticator app
await mfa.challengeAndVerify(data.id, '123456');
Sign-In with MFA
When a user with MFA signs in via email/password:
signIn.email() returns { requiresMfa: true }
- The app redirects to
/mfa-verify
- User enters their TOTP code
- The code is verified against the pending challenge
const { signIn, pendingMfaChallenge, mfa } = useAuth();
// During sign-in
const { requiresMfa } = await signIn.email(email, password);
if (requiresMfa) {
// Navigate to MFA verification page
}
// On the MFA verification page
if (pendingMfaChallenge) {
await mfa.verify(
pendingMfaChallenge.factorId,
pendingMfaChallenge.challengeId,
totpCode
);
}
Admin Enforcement
Super admins can require MFA for all users from the admin dashboard. This sets the mfa_enabled key in the app_settings table. When enforced, ProtectedRoute redirects users without MFA to the enrollment page.
Account Lockout
Brute force protection is built in. After 5 failed login attempts from the same email and IP address within a 15-minute window, the account is locked.
- Locked accounts receive an HTTP
429 Too Many Requests response with remaining_minutes
- Failed attempts are tracked in the
failed_login_attempts table
- Successful login clears all failed attempts for that email/IP pair
- Super admins can unlock accounts via the
admin_unlock_account(email) database function
Server-Side Auth
API requests include the user's JWT in the Authorization header. The server middleware validates it and makes the user available in route handlers:
const user = c.get('user');
if (!user) return c.json({ error: 'Unauthorized' }, 401);
The middleware chain runs on every request:
- Extract token from
Authorization: Bearer <token> header
- Validate with
supabase.auth.getUser() (server-side verification)
- Set context:
c.set('user', user) and c.set('supabase', client) where supabase is a user-scoped client that respects RLS
For operations that need to bypass RLS (admin tasks, webhooks), use the service role client:
import { supabaseAdmin } from '@/server/lib/supabase';
const { data } = await supabaseAdmin.from('organizations').select('*');