Better Auth
Modern authentication system with Better Auth
Beztack uses Better Auth as its authentication solution, providing a complete system with support for email/password, social auth, organizations, teams, and more.
Why Better Auth?
We chose Better Auth because:
- Framework Agnostic: Works with any JavaScript framework
- Type-Safe: Fully typed with TypeScript
- Modular: Plugin system to extend functionality
- Developer Experience: Intuitive and well-documented API
- Database: Native integration with Drizzle ORM
- Security: Built-in 2FA, email verification, and more
- Organizations: Native support for multi-tenancy
Configuration
Environment Variables
Set up the following variables in your .env file:
# Better Auth Configuration
BETTER_AUTH_SECRET=your-secret-key-here
BETTER_AUTH_URL=http://localhost:3000
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/beztack
# App Configuration
APP_NAME=beztackServer (Backend)
Server configuration is in apps/api/server/utils/auth.ts:
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin, organization, twoFactor } from "better-auth/plugins";
import { db } from "@/db/db";
import { schema } from "@/db/schema";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
},
socialProviders: {
// Configure social providers here
},
plugins: [
twoFactor({
issuer: process.env.APP_NAME || "beztack",
}),
admin(),
organization({
// Organization configuration
}),
],
});Client (Frontend)
Client configuration is in apps/ui/src/lib/auth-client.ts:
import { createAuthClient } from "better-auth/react";
import {
adminClient,
organizationClient,
twoFactorClient,
} from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000",
fetchOptions: {
credentials: "include",
},
plugins: [
twoFactorClient(),
adminClient(),
organizationClient({
teams: {
enabled: true,
},
}),
],
});Basic Authentication
User Registration
import { authClient } from "@/lib/auth-client";
// Register a new user
const { data, error } = await authClient.signUp.email({
email: "user@example.com",
password: "securePassword123",
name: "John Doe",
});Sign In
// Sign in with email/password
const { data, error } = await authClient.signIn.email({
email: "user@example.com",
password: "securePassword123",
});Sign Out
// Sign out
await authClient.signOut();Get Current User
// Using the React hook
import { useSession } from "@/lib/auth-client";
function MyComponent() {
const { data: session, isPending } = useSession();
if (isPending) return <div>Loading...</div>;
if (!session) return <div>Not authenticated</div>;
return <div>Welcome, {session.user.name}!</div>;
}Two-Factor Authentication (2FA)
Enable 2FA
// 1. Generate QR code for TOTP
const { data } = await authClient.twoFactor.enable({
password: "userPassword",
});
// data contains:
// - totpURI: URL for QR code
// - backupCodes: Backup codes
// 2. Verify and confirm 2FA
await authClient.twoFactor.verifyTotp({
code: "123456", // Code from TOTP device
});Sign In with 2FA
// 1. Normal sign in
await authClient.signIn.email({
email: "user@example.com",
password: "password",
});
// 2. If 2FA is enabled, provide TOTP code
await authClient.twoFactor.verifyTotp({
code: "123456",
});Disable 2FA
await authClient.twoFactor.disable({
password: "userPassword",
});Organizations and Teams
Better Auth includes full support for multi-tenancy with organizations and teams.
Create Organization
const { data } = await authClient.organization.create({
name: "My Company",
slug: "my-company",
});Invite Members
await authClient.organization.inviteMember({
organizationId: "org-id",
email: "member@example.com",
role: "member", // "owner", "admin", o "member"
});Accept Invitation
await authClient.organization.acceptInvitation({
invitationId: "invitation-id",
});Teams within Organizations
// Create a team
await authClient.organization.createTeam({
organizationId: "org-id",
name: "Development Team",
});
// Add member to team
await authClient.organization.addTeamMember({
teamId: "team-id",
userId: "user-id",
});Switch Active Organization
await authClient.organization.setActive({
organizationId: "org-id",
});Roles and Permissions
Predefined Roles
Better Auth includes three predefined roles:
- owner: Full control of the organization
- admin: Can manage members and settings
- member: Basic access
Check Role on Client
import { useActiveOrganization } from "@/lib/auth-client";
function AdminPanel() {
const { data: org } = useActiveOrganization();
if (!org || org.role !== "admin") {
return <div>You don't have permission</div>;
}
return <div>Admin panel</div>;
}Check Role on Server
import { auth } from "~/utils/auth";
export default defineEventHandler(async (event) => {
const session = await auth.api.getSession({
headers: event.headers
});
if (!session) {
throw createError({
statusCode: 401,
message: "Not authenticated",
});
}
// Check role in active organization
const member = await db
.select()
.from(member)
.where(
and(
eq(member.userId, session.user.id),
eq(member.organizationId, session.activeOrganizationId)
)
);
if (member[0]?.role !== "admin") {
throw createError({
statusCode: 403,
message: "Not authorized",
});
}
// Continue with logic
});Administration
Better Auth includes an admin plugin to manage users.
List Users (Admin)
const { data } = await authClient.admin.listUsers();Ban User (Admin)
await authClient.admin.banUser({
userId: "user-id",
reason: "Terms of service violation",
expiresAt: new Date("2024-12-31"), // Optional
});Unban User (Admin)
await authClient.admin.unbanUser({
userId: "user-id",
});Impersonate User (Admin)
await authClient.admin.impersonateUser({
userId: "user-id",
});Hooks and Middleware
Post-Registration Hook
In apps/api/server/utils/auth.ts:
import { sendEmail } from "@beztack/email";
import { createAuthMiddleware } from "better-auth/plugins";
export const auth = betterAuth({
// ... configuration
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path.includes("/sign-up")) {
const newSession = ctx.context.newSession;
if (newSession) {
await sendEmail({
type: "welcome",
to: newSession.user.email,
data: { username: newSession.user.name },
});
}
}
}),
},
});Verify Session on Server
import { auth } from "~/utils/auth";
export default defineEventHandler(async (event) => {
const session = await auth.api.getSession({
headers: event.headers
});
if (!session) {
throw createError({
statusCode: 401,
message: "Not authenticated",
});
}
return { user: session.user };
});Drizzle Integration
Better Auth uses Drizzle ORM for data persistence. The required tables are defined in apps/api/db/schema.ts:
user: User informationsession: Active sessionsaccount: Linked accounts (social providers)verification: Verification tokenstwoFactor: 2FA configurationorganization: Organizationsmember: Organization membersteam: Teams within organizationsteamMember: Team membersinvitation: Pending invitations
Cookie Configuration
For production with different subdomains:
export const auth = betterAuth({
// ... other options
advanced: {
cookies: {
sessionToken: {
attributes: {
secure: process.env.NODE_ENV === "production",
sameSite: "none", // Allow cross-site cookies
httpOnly: true, // Prevent XSS
},
},
},
},
});Best Practices
1. Always Verify Session on Server
Never trust client-side validation alone:
export default defineEventHandler(async (event) => {
const session = await auth.api.getSession({ headers: event.headers });
if (!session) {
throw createError({ statusCode: 401, message: "Not authenticated" });
}
// Your logic here
});2. Use Roles for Access Control
Implement role checks before sensitive operations:
if (member.role !== "admin" && member.role !== "owner") {
throw createError({ statusCode: 403, message: "Not authorized" });
}3. Configure Email Verification
For production, enable email verification:
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
}4. Use 2FA for Sensitive Accounts
Recommend or require 2FA for accounts with elevated permissions.
5. Handle Errors Appropriately
try {
await authClient.signIn.email({ email, password });
} catch (error) {
if (error.code === "INVALID_CREDENTIALS") {
// Show generic error for security
showError("Incorrect email or password");
}
}