Authentication¶
COORDINATOR uses BetterAuth for authentication with support for multiple authentication methods.
Supported Auth Methods¶
- Email/Password - Traditional credentials
- OAuth - Social login providers
- Passkeys - WebAuthn biometric authentication
- Two-Factor Authentication - TOTP-based 2FA
- Email OTP - One-time password via email
- Phone Number - SMS verification
BetterAuth Client¶
The auth client is configured in lib/auth-client.ts:
import { createAuthClient } from "better-auth/react";
import {
inferAdditionalFields,
adminClient,
twoFactorClient,
organizationClient,
passkeyClient,
jwtClient,
stripeClient,
phoneNumberClient,
emailOTPClient
} from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_AUTH_API_URL || "http://localhost:8787",
fetchOptions: { credentials: "include" },
plugins: [
inferAdditionalFields({
user: { position: { type: "string" }, tutorialProgress: { type: "json" } }
}),
adminClient(),
twoFactorClient(),
organizationClient({ teams: { enabled: true } }),
passkeyClient(),
jwtClient(),
stripeClient({ subscription: true }),
phoneNumberClient(),
emailOTPClient()
]
});
Route Protection¶
The proxy.ts middleware protects routes:
Protected Routes¶
Routes requiring authentication:
- /portal/**
- /pay/**
- /profile/**
Auth Routes¶
Routes that redirect authenticated users:
- /login
- /signup
Implementation¶
// proxy.ts
export async function proxy(request: NextRequest) {
const session = await betterFetch<Session>(
"/api/auth/get-session",
{ headers: { cookie: request.headers.get("cookie") || "" } }
);
if (isProtectedRoute && !session) {
return NextResponse.redirect(new URL("/login", request.url));
}
if (isAuthRoute && session) {
return NextResponse.redirect(new URL("/portal", request.url));
}
}
useAuth Hook¶
The hooks/use-auth.ts hook provides:
export function useAuth() {
const session = authClient.useSession();
const activeOrganization = authClient.useActiveOrganization();
// JWT token query (cached 10 minutes)
const { data: token } = useQuery({
queryKey: ["auth-token"],
queryFn: () => authClient.token(),
staleTime: 10 * 60 * 1000
});
// Sync active org with URL slug
useEffect(() => {
if (organizationSlug) {
authClient.organization.setActive({ organizationSlug });
}
}, [organizationSlug]);
return { session, activeOrganization, token };
}
Auth Components¶
Located in components/forms/auth/:
| Component | Purpose |
|---|---|
signin-form.tsx |
Email/password login |
signup-form.tsx |
New user registration |
reset-password-form.tsx |
Password recovery |
verify-code-form.tsx |
Email verification |
passkeys-form.tsx |
WebAuthn management |
oauth-button-form.tsx |
Social login buttons |
link-oauth-account-button-form.tsx |
Link social accounts |
logout-button.tsx |
Sign out |
update-user-form.tsx |
Profile updates |
Auth Flows¶
Sign Up Flow¶
- User submits email/password via
signup-form - BetterAuth creates user account
- Verification email sent
- User redirected to
/sign-up/verify-code - User enters OTP from email
- Account verified, redirected to
/portal
Sign In Flow¶
- User submits credentials via
signin-form - BetterAuth validates credentials
- Session cookie set
- If 2FA enabled, redirect to 2FA verification
- Redirected to
/portal
Passkey Flow¶
- User clicks "Add Passkey" in settings
- Browser prompts for biometric
- Credential stored in BetterAuth
- Future logins can use passkey
Session Management¶
Access session data throughout the app:
const { data: session } = authClient.useSession();
// User data
session?.user?.id
session?.user?.email
session?.user?.name
session?.user?.position // Extended field
// Session metadata
session?.session?.activeTeamId
Organization Context¶
The auth client manages organization membership:
// Get active org
const activeOrg = authClient.useActiveOrganization();
// List user's orgs
const orgs = authClient.useListOrganizations();
// Set active org (synced with URL)
authClient.organization.setActive({ organizationSlug: "my-org" });
// Set active team
authClient.organization.setActiveTeam({ teamId: "team-123" });
JWT Tokens¶
For API authentication, get a JWT token:
const { data: token } = useAuth();
// Use in API calls
fetch(url, {
headers: {
Authorization: `Bearer ${token}`
}
});
The token is automatically refreshed and cached by TanStack Query.