Skip to content

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

  1. User submits email/password via signup-form
  2. BetterAuth creates user account
  3. Verification email sent
  4. User redirected to /sign-up/verify-code
  5. User enters OTP from email
  6. Account verified, redirected to /portal

Sign In Flow

  1. User submits credentials via signin-form
  2. BetterAuth validates credentials
  3. Session cookie set
  4. If 2FA enabled, redirect to 2FA verification
  5. Redirected to /portal

Passkey Flow

  1. User clicks "Add Passkey" in settings
  2. Browser prompts for biometric
  3. Credential stored in BetterAuth
  4. 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.