# Authentication Auth is handled by [BetterAuth](https://better-auth.com) (`better-auth` package). ## Key Files | File | Purpose | |---|---| | `app/lib/auth.server.ts` | BetterAuth instance — providers, DB adapter, field mapping | | `app/lib/auth-client.ts` | Browser-side auth client (exported as `authClient`) | | `app/routes/api.auth.$.ts` | Catches all `/api/auth/*` requests and delegates to BetterAuth | | `app/lib/auth.ts` | Server helper: `requireLeagueAccess()` | ## Reading the Session In any route loader or action: ```ts import { auth } from "~/lib/auth.server"; const session = await auth.api.getSession({ headers: request.headers }); const userId = session?.user.id ?? null; // UUID string ``` `session` is `null` when the user is not logged in. ## User Fields BetterAuth's `user` object on the session exposes these fields: | Session field | DB column | |---|---| | `session.user.id` | `users.id` (UUID) | | `session.user.name` | `users.display_name` (mapped as `displayName`) | | `session.user.email` | `users.email` | | `session.user.image` | `users.image_url` (mapped as `imageUrl`) | Additional fields (`firstName`, `lastName`, `username`, `isAdmin`) are stored in `users` and accessible via DB lookup but are NOT on the session object by default. ## Admin Access `isAdmin` is a boolean on the `users` table. All `/admin` routes check it: ```ts const isAdmin = session ? await isUserAdmin(session.user.id) : false; ``` Do **not** rely on `session.user.isAdmin` — always re-read from the DB to pick up changes without requiring a new login. ## Ownership Validation Ownership is stored as a user UUID in FK columns (`teams.owner_id`, `leagues.created_by`, etc.). Compare against `session.user.id`: ```ts if (team.ownerId !== userId) throw new Response("Forbidden", { status: 403 }); ``` ## League Commissioner Access Commissioners are stored in the `commissioners` table. Use `requireLeagueAccess()` from `app/lib/auth.ts` to enforce that the current user is either a commissioner or a team owner: ```ts await requireLeagueAccess(args, { leagueId, seasonId, db }); ``` ## Auth Routes | Route | Purpose | |---|---| | `/login` | Email/password sign-in + Google/Discord OAuth | | `/register` | Email/password sign-up + OAuth | | `/forgot-password` | Request a password reset email | | `/reset-password` | Enter new password via reset token (link sent by email via Resend) | Password reset emails are sent via Resend from `noreply@brackt.com`. The reset link points to `/reset-password?token=…` using `BETTER_AUTH_URL` as the base. ## OAuth Providers Google and Discord are conditionally enabled when env vars are present: ``` GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET ``` Account linking is enabled — users who sign in with OAuth get their accounts linked if the email matches an existing email/password account (`trustedProviders: ["google", "discord"]`). ## Environment Variables ``` BETTER_AUTH_SECRET # required — random secret for signing sessions BETTER_AUTH_URL # required — app base URL, e.g. https://brackt.com RESEND_API_KEY # required for password reset emails ```