brackt/docs/agents/auth.md
2026-05-06 14:33:58 -07:00

3.1 KiB

Authentication

Auth is handled by BetterAuth (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:

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:

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:

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:

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 APP_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
APP_URL              # required — app base URL, e.g. https://brackt.com
RESEND_API_KEY       # required for password reset emails