Auth module
Better Auth server, client, routes, and account flow.
Auth module
Authentication is built on Better Auth. The server uses D1 (Drizzle) with email verification and password reset via the Mail module. The client uses better-auth/react's createAuthClient, integrated with TanStack Start via tanstackStartCookies.
Directory structure
src/auth/
├── auth.ts # Server: betterAuth instance (DB, email, social, plugins)
├── client.ts # Client: createAuthClient + plugins (admin, apiKey, inferAdditionalFields)
└── types.ts # Session / SessionUser types inferred from authRelated (outside src/auth/):
- API route:
src/routes/api/auth/$.ts— forwards GET/POST toauth.handler(request). - Middleware:
src/middlewares/auth-middleware.ts,src/middlewares/admin-middleware.ts. Both useauth.api.getSession({ headers })to obtain the session; there is no separatelib/session.ts. - Routes:
src/routes/auth.tsx(layout),src/routes/auth/login.tsx,src/routes/auth/register.tsx,auth/forgot-password,auth/reset-password,auth/error. - Components:
src/components/auth/(login-form, register-form, forgot-password-form, auth-card, error-card, login-wrapper, social-login-button, etc.);UserButton,SidebarUseruseSessionUserfrom@/auth/types. - Hooks:
src/hooks/use-auth.ts—useUserAccounts,useHasCredentialProvider. - DB user type:
src/db/types.tsexportsUser(table row); use for admin user list/detail. UseSessionUserfrom@/auth/typesfor current-session user (e.g. navbar, sidebar).
Server (auth.ts)
- baseURL: From
getBaseUrl(), backed by publicconfig.hostUrl. - database:
drizzleAdapter(getDb(), { provider: 'sqlite' }).getDb()(from@/db) uses the Worker'senv.DBD1 binding. - session: Cookie cache enabled;
expiresIn/updateAge/freshAgeconfigured; freshness check disabled to allow user deletion. - emailAndPassword:
- Enabled when
config.auth.enablePasswordSignInis true. requireEmailVerification: true.sendResetPassword: calls Mail modulesendEmail({ template: 'forgotPassword', ... }).
- Enabled when
- emailVerification:
sendVerificationEmail: callssendEmail({ template: 'verifyEmail', ... }).autoSignInAfterVerification: true.
- socialProviders: Google when
config.auth.enableGoogleSignInandserverEnv.GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETare set. - account: Account linking enabled when Google login is enabled; trusted provider
google. - user:
deleteUser.enabled: true. - plugins:
tanstackStartCookies()— cookies/session with TanStack Start on Cloudflare.admin()— user management, ban/unban, roles;bannedUserMessageand optionaldefaultBanExpiresIn.apiKey()— Better Auth plugin remains a technical compatibility item only; user-facing API key management uses the sourceapi_keybusiness table.oneTap()— Google One Tap server plugin, enabled with Google sign-in.emailOTP()— email OTP login.emailHarmony()— email normalization/validation;allowNormalizedSignin: false.
- onAPIError:
errorURL: '/auth/error'; optionalonErrorlogging.
Client (client.ts)
createAuthClient({ baseURL: getBaseUrl(), plugins: [adminClient(), apiKeyClient(), oneTapClient({ clientId: config.auth.googleClientId, ... }), emailOTPClient(), inferAdditionalFields<typeof auth>()] })exported asauthClient.oneTapClient()is included only whenconfig.auth.enableGoogleSignInandconfig.auth.googleClientIdare present.- Current prompt options set
fedCM: falseandmaxAttempts: 2.
- Typical usage:
authClient.useSession()— current session (includes user with role when admin plugin is used).authClient.signIn.email(),authClient.signUp.email(),authClient.signOut().authClient.emailOtp.sendVerificationOtp()/authClient.signIn.emailOtp()— email OTP login.authClient.updateUser({ name, image })— e.g. profile/avatar.authClient.listAccounts()— used byuseUserAccountsinuse-auth.ts.
Requests go to baseURL + /api/auth/* and are handled by auth.handler.
Session checks for protected APIs
Session is obtained with auth.api.getSession({ headers }) (from @/auth/auth). There is no shared lib/session.ts; each protected API route and middleware calls getSession directly.
- Middleware (
auth-middleware.ts,admin-middleware.ts): callawait auth.api.getSession({ headers }); redirect to login or dashboard when null or when role is not admin. - API routes (e.g.
routes/api/storage/file.tsfor file serving): callauth.api.getSession({ headers })and return 401/403 when session is null or ownership fails. - Server functions (e.g.
src/api/user-files.ts): useauthApiMiddlewareso unauthenticated calls get 401. Upload is implemented asuploadUserFileserver function, not an API route.
Server functions that need a session (e.g. listUsers in src/api/users.ts) use middleware such as adminApiMiddleware, which runs on the server and obtains the session the same way.
Route protection (middleware)
- authMiddleware (
src/middlewares/auth-middleware.ts): Requires an authenticated user; redirects toRoutes.Loginwhen there is no session. Use in route definitions viaserver: { middleware: [authMiddleware] }(e.g. dashboard, settings). - adminMiddleware (
src/middlewares/admin-middleware.ts): Requiressession.user.role === 'admin'; redirects to login if not signed in, otherwise to dashboard if not admin. Use on admin routes (e.g./admin/users).
Configuration and environment
- websiteConfig.auth (
src/config/website.ts):enable,enableGoogleLogin,enableCredentialLogin. - D1: Configure
d1_databasesinwrangler.jsoncwith binding nameDB;getDb()usesenv.DB. - Mail: Verification and password reset depend on the Mail module. (see Mail).
- Google OAuth: Set
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETin server env. Full list: Env.
Database tables
Auth tables are defined in src/db/auth.schema.ts and aligned to the source Next schema: user, session, account, and verification. Target-only normalizedEmail and customerId fields are intentionally not part of user. User-facing API keys are stored in src/db/app.schema.ts as api_key. Schema is re-exported from src/db/schema.ts. Migrations and Drizzle usage are described in the DB module.