Beztack
Authentication

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=beztack

Server (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 information
  • session: Active sessions
  • account: Linked accounts (social providers)
  • verification: Verification tokens
  • twoFactor: 2FA configuration
  • organization: Organizations
  • member: Organization members
  • team: Teams within organizations
  • teamMember: Team members
  • invitation: Pending invitations

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");
  }
}

Resources