Authentication
This project uses better-auth for authentication - a comprehensive TypeScript-first authentication framework.
Features
- Email/Password Authentication with email verification
- Session Management with automatic token refresh
- Type-Safe client and server with end-to-end TypeScript support
- Password Reset Flow for forgotten passwords
- Social Login (GitHub, Google, etc.) - optional
- Built-in Security with battle-tested best practices
Authentication Flow
1. User Registration
User fills registration form
↓
POST /auth/sign-up/email
↓
User created in database
↓
Verification email sent
↓
User clicks verification link
↓
POST /auth/verify-email
↓
Email marked as verified2. User Login
User enters credentials
↓
POST /auth/sign-in/email
↓
Credentials verified
↓
Session created
↓
User authenticated3. Session Management
better-auth automatically handles JWT tokens and session management. Tokens are refreshed automatically when needed.
Shared Authentication Configuration
Authentication configuration is shared across the API and frontend apps to keep validation, profile requirements, and client behavior consistent. The source of truth lives in the workspace packages.
Shared User Profile Rules
packages/shared/config/user-profile.ts defines the reusable constraints that power both backend validation and frontend UI:
USERNAME_CONFIG(length, pattern, description) is consumed by:apps/api/src/lib/auth.ts(better-authusernameplugin)packages/shared/auth/user-profile-fields.ts(Zod schema)apps/frontend/src/pages/profile/complete.tsx(form validation)
REQUIRED_USER_FIELDSdrives profile completion:apps/api/src/config/required-fields.tschecks missing fieldsapps/frontendprofile completion UX ensures the same fields are required
When changing required fields
- Update
packages/shared/config/user-profile.ts. - Add validators in
packages/shared/auth/user-profile-fields.tsif needed. - Update the user profile schema in the API database (if you add columns).
- Update the profile completion UI in
apps/frontend/src/pages/profile/complete.tsx.
Shared Auth Client Setup
Frontend apps should create a better-auth client instance using the shared helper in packages/frontend-common/auth:
import { createAuthClientInstance } from "frontend-common/auth";
export const authClient = createAuthClientInstance(
import.meta.env.VITE_API_BASE_URL
);The shared client configuration:
- Sets
basePathto/authso all apps hit the same API routes. - Enables the shared plugins (admin, magic-link, username).
Requirements
To keep shared authentication configuration working across apps:
- Ensure the consuming app depends on
sharedandfrontend-commonworkspace packages. - Configure
VITE_API_BASE_URLin frontend/admin so the shared client points at the API. - Keep API
API_BASE_URLaligned with the API URL, andFE_BASE_URL/CORS_ALLOWLISTED_ORIGINSaligned with the frontend URL.
API Endpoints
All endpoints are handled by better-auth automatically at /auth/*:
| Endpoint | Method | Description |
|---|---|---|
/auth/sign-up/email | POST | Register new user with email/password |
/auth/sign-in/email | POST | Login with email/password |
/auth/sign-out | POST | Logout current user |
/auth/verify-email | POST | Verify email with token |
/auth/get-session | GET | Get current session |
/auth/forget-password | POST | Request password reset |
/auth/reset-password | POST | Reset password with token |
Protecting Routes (Backend)
Use the authMiddleware to protect API routes:
import { authMiddleware } from "../middlewares/auth";
import type { AuthMiddlewareEnv } from "../middlewares/auth";
import type { LoggerMiddlewareEnv } from "../middlewares/logger";
const router = new Hono<LoggerMiddlewareEnv & AuthMiddlewareEnv>()
.get("/protected", authMiddleware(), (c) => {
// Access authenticated user
const user = c.var.user;
const session = c.var.session;
return c.json({
message: "Protected data",
user: {
id: user.id,
email: user.email,
emailVerified: user.emailVerified,
},
});
});Type Safety: The middleware provides type-safe access to user and session via c.var.
Protecting Routes (Frontend)
Use the ProtectedRoute component to wrap pages that require authentication:
import { ProtectedRoute } from "@frontend/src/components/ProtectedRoute";
export default function DashboardPage() {
return (
<ProtectedRoute>
<div>
This content is only visible to authenticated users
</div>
</ProtectedRoute>
);
}How it works:
- Checks if user is authenticated using
useSessionhook - Shows loading state while checking session
- Redirects to
/auth/loginif not authenticated - Renders children if authenticated
Using Auth in Components
Get Current Session
import { useSession } from "@frontend/src/lib/auth-client";
export function MyComponent() {
const { data: session, isPending, error } = useSession();
if (isPending) return <div>Loading...</div>;
if (!session) {
return <Link to="/auth/login">Please login</Link>;
}
return (
<div>
<p>Welcome, {session.user.email}</p>
<p>Email verified: {session.user.emailVerified ? "Yes" : "No"}</p>
</div>
);
}Sign Out
import { signOut } from "@frontend/src/lib/auth-client";
export function LogoutButton() {
return (
<button onClick={() => signOut()}>
Sign Out
</button>
);
}Social Login (Optional)
If you've configured OAuth providers (like GitHub):
import { signIn } from "@frontend/src/lib/auth-client";
export function SocialLogin() {
const handleGitHubLogin = async () => {
await signIn.social(
{ provider: "github" },
{
onSuccess: () => {
navigate("/dashboard");
},
onError: (error) => {
console.error("Login failed:", error);
},
}
);
};
return (
<button onClick={handleGitHubLogin}>
Sign in with GitHub
</button>
);
}Email Verification
Email verification is enabled by default. Users must verify their email before they can log in.
Setup
Configure SMTP in your .env file:
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_USER="your-email@gmail.com"
SMTP_PASSWORD="your-app-password"
SMTP_FROM="Fullstack Bun <noreply@example.com>"Development
If SMTP is not configured, the verification URL will be logged to the console instead:
📧 Email Verification URL for user@example.com:
http://localhost:5173/auth/verify-email?token=abc123...Setting Up Social Login
GitHub OAuth
Create GitHub OAuth App:
- Go to https://github.com/settings/developers
- Click "New OAuth App"
- Set Authorization callback URL:
http://localhost:3001/auth/callback/github
Add Credentials to
.env:txtGITHUB_CLIENT_ID="your-client-id" GITHUB_CLIENT_SECRET="your-client-secret"Use in Frontend:
typescriptawait signIn.social({ provider: "github" });
Security Best Practices
Production Checklist
- ✅ Use HTTPS in production
- ✅ Keep
BETTER_AUTH_SECRETsecure and never commit it - ✅ Rotate secrets periodically (at least annually)
- ✅ Use strong password requirements (enforced by better-auth)
- ✅ Enable email verification (enabled by default)
- ✅ Use secure SMTP credentials
- ✅ Set up rate limiting on auth endpoints (recommended)
- ✅ Monitor failed login attempts
Password Requirements
better-auth enforces:
- Minimum 8 characters
- Additional complexity rules can be configured
Session Security
- Sessions are stored securely with JWT
- Access tokens expire after 15 minutes
- Refresh tokens are used to obtain new access tokens
- Sessions can be invalidated on logout
Troubleshooting
Email Verification Not Working
- Check SMTP Configuration: Ensure all SMTP environment variables are set correctly
- Check Logs: Look for email sending errors in console output
- Check Spam Folder: Verification emails might be marked as spam
- Development Mode: Use the console-logged verification URL
User Can't Login
- Email Not Verified: User must verify email before login
- Incorrect Credentials: Check that email and password are correct
- Check Database: Ensure user exists in the
userstable
Session Not Persisting
- Check CORS: Ensure
CORS_ALLOWLISTED_ORIGINSincludes your frontend URL - Check Cookies: Some browsers block third-party cookies
- Check Network: Look for failed session requests in browser DevTools
API Reference
For more details, see the better-auth documentation.