Documentation
Better Auth
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
| Feature | Status | Notes |
|---|---|---|
| Email / password sign-up | Enabled | Verification required before dashboard access |
| Email / password sign-in | Enabled | Redirects unverified users to /verify-email |
| Google OAuth | Optional | Enabled when GOOGLE_* env vars are set |
| GitHub OAuth | Optional | Enabled when GITHUB_* env vars are set |
| Email verification | Required | Sent on sign-up; resend on sign-in if still unverified |
| Password reset | Enabled | Requires Resend (RESEND_API_KEY, EMAIL_FROM) |
| Change password | Enabled | Settings → Security; revokes other sessions |
| Sign out | Enabled | User menu in the dashboard header |
| Session management | Enabled | List and revoke sessions in Settings → Security |
| Account linking | Enabled | Google 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:
src/proxy.ts— edge gate for/dashboardand/dashboard/*. Redirects unauthenticated users to/sign-inand unverified users to/verify-email.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.
| Step | What happens |
|---|---|
| Sign up | Better Auth sends a verification link via sendVerificationEmail |
| Click link | User is verified; they can sign in |
| Unverified sign-in | Redirect 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
| Route | Purpose |
|---|---|
/forgot-password | Request a reset link |
/reset-password?token=… | Set a new password from the email link |
Flow:
- User submits email on
/forgot-password - Client calls
authClient.requestPasswordResetwithredirectTopointing at/reset-password - Better Auth sends the email via
sendResetPasswordinauth.ts - 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
| File | Change this when you want to… |
|---|---|
auth.ts | Enable/disable providers, toggle verification, customize email hooks, add Better Auth plugins, change account linking |
session.ts | Adjust how server components read the session (getCachedSession) |
client.ts | Configure the browser auth client (base URL is inferred automatically) |
social.ts | Change how Google/GitHub “configured” checks work |
safe-client-error.ts | Edit 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
| Path | Change 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.tsx | Shared 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
| File | Change this when you want to… |
|---|---|
actions/security.ts | Password change, session revoke server actions |
lib/security-settings.ts | Load sessions for the Security settings page |
components/settings/security-settings-page.tsx | Security UI (password form, session list) |
components/settings/profile-settings-page.tsx | Profile name and avatar |
actions/profile.ts | Profile 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
| File | Change this when you want to… |
|---|---|
src/proxy.ts | Edge redirects for /dashboard (session + verification gate) |
src/app/dashboard/layout.tsx | Dashboard shell gate and user bootstrap |
Transactional email (auth-related)
| File | Change this when you want to… |
|---|---|
src/lib/email/index.ts | Verification, reset, welcome send functions |
src/lib/email/templates/verification-email.tsx | Verification email content |
src/lib/email/templates/password-reset-email.tsx | Reset email content |
src/lib/email/templates/welcome-email.tsx | Welcome email content |
src/lib/email/templates/shared.tsx | Email 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.