Documentation

Better Auth

How Motoko Base handles authentication with Better Auth — email/password, OAuth, sessions, protected routes, verification, and account management.

Open inChatGPT (opens in a new tab)Claude (opens in a new tab)Cursor (opens in a new tab)How Motoko Base handles authentication with Better Auth — email/password, OAuth, sessions, protected routes, verification, and account management.

Motoko Base uses Better Auth for authentication. Sessions are stored in PostgreSQL via the Drizzle adapter. There is no separate auth service — Postgres plus env vars are enough for local development.

Auth API routes are mounted at /api/auth/*. The dashboard requires a verified session before rendering.

What ships out of the box

FeatureStatusNotes
Email / password sign-upEnabledVerification required before dashboard access
Email / password sign-inEnabledRedirects unverified users to /verify-email
Google OAuthOptionalEnabled when GOOGLE_* env vars are set
GitHub OAuthOptionalEnabled when GITHUB_* env vars are set
Email verificationRequiredSent on sign-up; resend on sign-in if still unverified
Password resetEnabledRequires Resend (RESEND_API_KEY, EMAIL_FROM)
Change passwordEnabledSettings → Security; revokes other sessions
Sign outEnabledUser menu in the dashboard header
Session managementEnabledList and revoke sessions in Settings → Security
Account linkingEnabledGoogle and GitHub link to the same user when trusted

Social sign-in buttons stay disabled until OAuth credentials are configured. See OAuth Setup for step-by-step provider setup.

Email and password

Sign up (/sign-up) collects name, email, and password. Better Auth creates the user and sends a verification email when Resend is configured.

Because requireEmailVerification is on, sign-up does not auto-sign the user in. After sign-up they see a “Check your email” screen and must verify before accessing /dashboard.

Sign in (/sign-in) accepts email and password. On success, the client redirects to /dashboard. If the email is unverified, Better Auth returns 403 EMAIL_NOT_VERIFIED and the form redirects to /verify-email.

Password rules for sign-up and change-password are validated in src/features/dashboard/schemas/security.ts (MIN_PASSWORD_LENGTH = 8 by default).

Google and GitHub OAuth

Both providers are wired in src/lib/auth/auth.ts and activated only when env vars are present. The client uses authClient.signIn.social({ provider: "google" | "github" }) from the sign-in and sign-up forms.

OAuth users are typically marked verified by the provider. Account linking is enabled for Google and GitHub so a returning social user can attach to an existing email account when Better Auth trusts the provider.

Full provider setup: OAuth Setup.

Sessions

Sessions are stored in the session table (src/lib/db/schema/auth.ts). Each session has a token, expiry, IP, and user agent.

Server-side reads use getCachedSession() from src/lib/auth/session.ts. It wraps auth.api.getSession() in React cache() so layout, pages, and server actions in the same request share one lookup.

Client-side calls use authClient from src/lib/auth/client.ts (Better Auth React client).

Example — check session in a server component or action:

import { getCachedSession } from "@/lib/auth/session";

const session = await getCachedSession();
if (!session?.user?.id) {
  // not signed in
}

Protected routes

Dashboard routes under /dashboard are protected in two layers:

  1. src/proxy.ts — edge gate for /dashboard and /dashboard/*. Redirects unauthenticated users to /sign-in and unverified users to /verify-email.
  2. src/app/dashboard/layout.tsx — server layout that repeats the same checks before rendering the shell.

Individual pages and server actions also call getCachedSession() and redirect or return errors when needed. This defense-in-depth pattern keeps API routes and actions safe even if a page forgets to check.

To protect a new route, either place it under src/app/dashboard/ (inherits layout + proxy) or call getCachedSession() and redirect manually.

Email verification

Verification is required for dashboard access.

StepWhat happens
Sign upBetter Auth sends a verification link via sendVerificationEmail
Click linkUser is verified; they can sign in
Unverified sign-inRedirect to /verify-email
Resend/verify-email calls authClient.sendVerificationEmail

Email delivery requires RESEND_API_KEY and EMAIL_FROM. Without Resend, the app boots but verification emails are not sent — configure email before testing sign-up locally.

Verification email content lives in src/lib/email/templates/verification-email.tsx. The send hook is in src/lib/auth/auth.ts under emailVerification.sendVerificationEmail.

Password reset

RoutePurpose
/forgot-passwordRequest a reset link
/reset-password?token=…Set a new password from the email link

Flow:

  1. User submits email on /forgot-password
  2. Client calls authClient.requestPasswordReset with redirectTo pointing at /reset-password
  3. Better Auth sends the email via sendResetPassword in auth.ts
  4. User opens the link and submits a new password via authClient.resetPassword

Reset emails require Resend. The forgot-password page checks email configuration before sending.

Change password

Signed-in users change their password at Settings → Security (/dashboard/settings/account/security).

The UI calls updatePasswordAction in src/features/dashboard/actions/security.ts, which uses auth.api.changePassword when the user already has a password credential.

Password length limits come from src/features/dashboard/schemas/security.ts.

Sign out

Sign out is triggered from the dashboard account menu. src/features/dashboard/components/sidebar/account-menu.tsx calls:

await authClient.signOut({
  fetchOptions: {
    onSuccess: () => {
      router.push("/sign-in");
      router.refresh();
    },
  },
});

Analytics and Sentry user context are cleared on sign-out in the same handler.

Account and session management

Profile (name, avatar) — Settings → Profile (src/features/dashboard/components/settings/profile-settings-page.tsx).

Security (password, sessions) — Settings → Security:

  • Change password
  • List active sessions (browser, OS, last activity)
  • Revoke individual sessions
  • Sign out all other sessions

Server logic is in src/features/dashboard/actions/security.ts (updatePasswordAction, revokeSessionAction (sessions load via src/features/dashboard/lib/security-settings.ts)).

On sign-up, a welcome email is sent from the databaseHooks.user.create.after hook in auth.ts. When Polar billing is configured, the Polar Better Auth plugin creates a billing customer on sign-up (src/lib/billing/providers/polar/auth-plugin.ts).


Where do I change this?

Motoko Base splits auth across server infrastructure (src/lib/auth/), auth pages (src/app/…), and dashboard settings (src/features/dashboard/). There is no separate src/features/auth/ folder — auth UI lives in app routes; account management lives under dashboard settings.

src/lib/auth/ — server auth core

FileChange this when you want to…
auth.tsEnable/disable providers, toggle verification, customize email hooks, add Better Auth plugins, change account linking
session.tsAdjust how server components read the session (getCachedSession)
client.tsConfigure the browser auth client (base URL is inferred automatically)
social.tsChange how Google/GitHub “configured” checks work
safe-client-error.tsEdit user-facing error messages for sign-in, sign-up, OAuth

The catch-all API handler is src/app/api/auth/[...all]/route.ts — you rarely need to edit it.

Database tables: src/lib/db/schema/auth.ts (user, session, account, verification).

Auth pages — sign-in, sign-up, recovery

PathChange this when you want to…
src/app/sign-in/Sign-in form behavior, post-login redirect, OAuth handlers
src/app/sign-up/Sign-up form, pending-verification UI
src/app/verify-email/Resend verification UX
src/app/forgot-password/Forgot-password form
src/app/reset-password/Reset-password form
src/components/ui/auth-section-3.tsxShared auth layout (fields, social buttons, styling)

Sign-in and sign-up pages pass googleEnabled / githubEnabled from isGoogleAuthConfigured() and isGitHubAuthConfigured() so buttons disable automatically when OAuth is not configured.

src/features/dashboard/ — account and sessions

FileChange this when you want to…
actions/security.tsPassword change, session revoke server actions
lib/security-settings.tsLoad sessions for the Security settings page
components/settings/security-settings-page.tsxSecurity UI (password form, session list)
components/settings/profile-settings-page.tsxProfile name and avatar
actions/profile.tsProfile update server actions

Password length limits for settings UI: src/features/dashboard/schemas/security.ts.

Route: src/app/dashboard/settings/account/security/page.tsx loads sessions server-side and renders SecuritySettingsPage.

Route protection

FileChange this when you want to…
src/proxy.tsEdge redirects for /dashboard (session + verification gate)
src/app/dashboard/layout.tsxDashboard shell gate and user bootstrap
FileChange this when you want to…
src/lib/email/index.tsVerification, reset, welcome send functions
src/lib/email/templates/verification-email.tsxVerification email content
src/lib/email/templates/password-reset-email.tsxReset email content
src/lib/email/templates/welcome-email.tsxWelcome email content
src/lib/email/templates/shared.tsxEmail brand name and layout

Env vars for auth: see Environment Variables.


Next steps

OAuth Setup — Step-by-step Google and GitHub configuration.

Environment Variables — Full env reference including BETTER_AUTH_* and OAuth keys.